16.1.1 高性能语言的秘密
有一个比较常见的误解:语言能够决定程序的执行速度。优秀并且有用的性能测试经常是被高度特化的。这些测试以非常特殊的 case 来对性能进行测量,而非更为通用的测定方式。因此当别人给出一些性能的语句时,最好能将测试的场景细节和结果一并给出。通过详细地描述我们能够创建类似的评测系统并运行差不多的测试,从而得到可比较的结论。
在一些 case 下,用 C 写的程序可能会被类似行为的 Java 程序胜过。而这种情况和语言本身没什么关系。
举个例子,malloc 这种典型的实现有一个特点:没有办法预测该实现的执行时间。一般来说,时间取决于当前的堆状态:有多少 block 存在,堆的碎片化程度怎么样等等。很多情况下在堆上分配内存都会比在栈上要得多一些。然而在典型的 JVM 实现中,内存分配非常快速。这是因为 Java 有非常简单的堆结构。简化来说,就只是一段内存区域和内部的一个指针,该指针从空闲区域中划出一块占用区域。所以这种情况下分配内存只是向 free 区域移动指针,操作起来非常快。
不过这种做法也有成本,我们需要释放不再使用的内存块时会触发垃圾回收,垃圾回收会直接停止你的程序,而这个停止的时间又是不可预知的。
我们设想一种从来不进行垃圾回收的场景,例如,一个程序先申请内存,然后进行计算,执行计算之后就退出并销毁所有占用过的地址空间,而不进行垃圾回收。这种情况下 Java 程序因为只有内存释放的开销,所以执行速度是会比较快的。然而如果我们使用自定义的内存分配器,来满足我们在特殊任务下的特殊需求的话,我们也可以在 C 语言中进行类似的技巧,从而大幅度降低内存管理开销。
此外,由于 Java 一般是动态翻译和执行的,虚拟机能够在运行期根据程序执行过程进行优化。例如,两个经常被一起执行的方法,可以放在内存中临近位置,这样它们就可以被一起放进 cache。为了达到这样的效果,需要收集必要的程序执行追踪信息,而这一点只在执行期才可能完成。
C 语言和其它语言最大的区别实际上是其非常透明的程序成本模型。无论你在写的是什么样的程序,都可以很容易地推断出这段程序的汇编形式。而像 Java 或者 C# 这种和运行时绑定的语言,或者像 C++ 这样提供了多种抽象例如虚继承的语言,就很难预测了。在 C 语言中的抽象就只有 struct 和 union 还有函数。
只是单纯地把 C 程序翻译成机器指令的话,一般程序执行会非常缓慢。哪怕是用优化过的编译器,生成的代码也一样如此。一般情况下,程序员对计算机底层架构的了解肯定没有编译器深刻,所以一般程序的底层优化一般都是由编译器来做,而人显然是没法和编译器竞争的。否则的话,对于程序员来说,就需要针对不同的平台或者编译器,去编写不同的代码,并且可能在降低程序可读性和可维护性的前提下去加速代码的运行了。再强调一次,性能测试对于每一个人来说都是必要的。