在上一部分中,我花了大量的精力强烈暗示使得C++如此成功的并不是由于所谓性能上的优势,而是其规范并升华了C的最佳实践。现在我要用同样的精力来证明C++在性能上的专门考虑和设计以及实践中的优越表现。C++根植与C的肥沃土壤,天然具有C所拥有的直接与物理硬件交互的优势。更要提到的是,每一个C++新特性的引入,无不是经过了详细的性能权衡。毫不奇怪,C++在绝大多数的情况下可以取得与C比肩的性能,并且有所超过。
性能的最终瓶颈必然取决于硬件能力。这样,优化性能的途径不外乎两点:
1. 充分发挥硬件的优势。
2. 不做无用功。
在第一点上,C和C++不分伯仲。原因是,编译器后端使用的往往一个完整支持的CPU集合的大部分公共指令,这就意味着某种可以用于特定CPU的特别优化指令根本就不可能出现在编译后的二进制代码中。这同时也就说明了为什么需要内联的汇编,而C和C++都完美支持这个特性。另一方面,C和C++编译器一般使用同样的后端,这样更模糊了C和C++的界限。同样有效的C代码,使用C++编译器和C编译器产生的最终结果基本完全一致,甚至更好,后面有详述。
第二点看起来非常简单,但是由于做无用功的可能性成千上万,想做到不做无用功也就比初看起来的要困难的多。我们先简单分析一下这是为什么。第一,现代基于模块的编程实践非常强调封装,这基本上都是以一定的性能损失为代价的。对于编译器来说,那就是,你的代码不可能超出编译器和其基本库的实现限制;第二,选择合适的算法有时候很难,这决定了实现的性能表现。本质上,就是使用更好的避免的做无用功的方法。使用简单的排序算法说明。冒泡算法之所有低效,是因为尽管我们通过比较知道a>b,b>c,但是我们还需要比较a和c的关系,其实这完全是浪费。快速排序则不同,它使用一个值来吧整个排序集合分为两部分,大于它的和小与它的。这样,这两个集合中的任何元素都不必在进行比较,这就是不做无用功。仔细思考一下其他的常用算法,你就可以很清晰地认识到都是这样。这就是我们一直强调算法是性能的关键的原因,因为标准算法都是经过详细设计的,实践证明正确的,做无用功最少的代码抽象。第三,无论从何种尺度,避免重复都要比预期的要困难。重复可能表现为:数据重复(同样程序状态被保存在多个位置,它们之间需要更新和同步),执行重复(由于诸如缓存频繁失败而导致的低性能代码被重复调用),结构重复(同样的代码被重复执行而具有相同的副作用)等,这仅仅是运行态的一般情况。实现的重复则会导致的执行体体积庞大,引用冗余,在导致性能问题的同时引出维护的灾难。
其实这些都说明,在相同的层面上,性能其实和你选择工具语言没有什么直接的关系。C和C++无疑是在相同的层面上的。我们已经知道,C++提供了现代的构造,满足超大规模的设计和实现的基本要求。但是,C++是怎么在提升代码可管理性的基础上,可以保证与C基本一致(差别小于5%)的性能的呢?
1. 使用(更)强的类型。C++和C都是强类型的语言,但是C++做的更好。强类型带来的好处是,类型判断和检查可以在编译期执行,而不是运行时。这就意味着目标代码的体积更小,逻辑更简单。
2. 需要时再计算(延迟求值)。这是最常用的常规性能优化方法。程序的一致执行会话中,可能某些代码段并不会被执行到,那么这些代码所需要的数据就没有必要产生或者计算。mutable和const关键字提供了基本的支持。
3. 需要时才声明对象。这样可以避免当条件不满足时构造和销毁对象的开销,同时保证的最小的对象作用域。
4. 避免没必要的对象复制。返回对象的函数调用一般都会产生一个临时对象,当函数调用完毕之后,该临时对象就会被销毁。这虽然完全符合C++ 的对象语义,但是是低效的。一般的C++编译器都会执行返回值优化来消除这个额外的对象构造。从C++语言来说,这样的临时变量其实可以绑定到一个常量引用,在该常量引用失效之前,这个零时对象一直存在。然而,仍然存在着一些场景,会不可避免地产生中间对象的而导致的低效,C++0x通过引入右值引用来试图消除这个最后的可能性。
5. 基于模板的标准程序库和算法。这是C++中最令人叫绝的地方。它是执行效率和易用性的完美折中。和C基于值语义类似,所用的标准容器都保存一份数据或者数据指针的拷贝。模板在避免了大量的代码重复的同时,通过完全媲美手写算法的性能,这就是C++语言。
C++在性能问题上被人诟病,除了明显的无知,还有一些深层次的原因。那就是对C++特性的滥用。这里讨论几个最常听到的arguments:
a. C++的虚函数机制。可能你常常看到有人煞有介事地评论调用一个虚函数会导致一直额外的查表操作,这个是如何导致了他的代码的性能低下。事实是:除非他的代码根本不需要运行时多态而滥用了虚拟机制,否则C++编译器的实现基本上可以说是最直接且高效的,比如使用散列来索引函数的地址。恰当的比喻是,你要切牛肉,非要使用关老爷的青龙偃月,还要抱怨刀锋划了案板。
b. 异常机制。异常恐怕是C++里最没有被广泛使用的最好的特性了。由于异常要求非常不同的程序观念,有一些公司甚至如洪水猛兽般避之不及。不幸的是,C++标准库也使用了异常,那就意味着C++标准库最好也就不要碰了。异常导致的问题主要是代码执行管理和软件架构中错误处理策略的更本性变革,但是从来不是因为其性能问题。是的,异常的抛出和捕获会导致低性能。但是真正需要异常发挥作用的地方可能只有程序执行逻辑的1%。形象的比喻是,如果过年吃一顿好的也被认为是浪费,那就是不辨是非了。
c. 对象的自动构造,析构和复制。复制我们不说,因为C也支持数据结构(struct)的直接赋值。对象的自动构造和析构之所以必要,因为在某些情况下,我们需要。当我们不需要的时候,它们完全可以是没有的。如果可以理解C中要求变量声明后必须赋初值的最佳实践,也就可以明白C++怎么把这样的最佳时间制度化了。
该是那就老话,性能是设计出来的,不是实现出来的。有关C++性能的讨论可以休矣!