[Herb Sutter 的名作More Exceptional C++中文版即将出版。作为本书译者,我很高兴将本书推荐给大家。征得华中科技大学出版社同意,我将公开部分译稿,敬请大家批评指正。]
优化与性能
对程序员来说,效率总是很重要。在C和C++的传统中,效率是重要支柱之一,“不要为没有使用的东西支付任何成本”这一指导原则——也称为零成本原则——总是语言设计和程序库设计的中心,而且,在很大程度上也确实得到了实现。
在本章以及书后的两个附录中,我们对一些重要的C++优化问题进行了深入的观察,并分析了它们对现实世界代码的影响。应该在何时优化你的代码?如何优化?inline到底做了些什么?为什么花哨的优化能够(而且确实会)让我们陷入麻烦?最后一点,并且对我来说最有趣的一点:如果你是在写多线程代码,上面一些问题的答案会如何发生变化?毕竟,我们关心的是现实世界中的效率问题;虽然C++标准对线程避而不谈,但在程序设计领域的第一线,每天都有越来越多的程序员在写多线程的C++代码。他们会关心这些问题的答案。
条款12:内联
难度:4
和大多数人的看法相反,关键字inline其实并不是什么魔法。事实上,只是在被恰当运用的时候,它才会成为有用的工具。问题是:什么时候该使用它呢?
1. inline有什么作用?
2. 将函数内联会提高效率吗?
3. 何时应当决定使用内联函数?如何决定?
解答
1. inline有什么作用?
将一个函数声明为inline意味着告诉编译器:编译器可以将这个函数代码的拷贝直接放在每一个使用这个函数的地方。编译器可以选择这么做,或不这么做;如果编译器确实这么做了,将会避免函数调用的发生。
2. 将函数内联会提高效率吗?
不一定。
首先,在回答这个问题之前,如果不先问问自己到底想优化“什么”,你就会落入一个著名的陷阱。第一个问题应该是:“你所说的效率指的是什么?”在上面的提问中,所谓的效率指的是程序体积吗?抑或是内存占用?执行时间?开发速度?编译时间?或是其它什么东西?
其次,和大多数人的看法相反:对于效率的各个方面,内联可能使之改善,也可能使之恶化:
a) 程序体积。许多程序员认为,内联一定会增加程序的体积,因为程序拥有的将不只是函数代码的一份拷贝,编译器会在使用了那个函数的每个地方生成一份拷贝。通常来说这是对的,但并非总是如此。如果和“编译器为了执行函数调用而不得不生成的代码”的体积相比,内联函数的体积比它还小,那么,内联会减小程序体积。
b) 内存占用。除了(上面所说的)基本程序体积外,内联通常对程序的内存使用没有影响,或极少有影响。
c) 执行时间。很多程序员认为,将函数内联一定会提高运行速度,因为它避免了函数调用的开销;而且,透过了函数调用这层“障碍”,编译器的优化程序就有了更多大显身手的机会。这可能是正确的,但不总是正确:如果函数不是被极其频繁地调用,整个程序的执行时间通常不会有明显的改善。实际上,事情有可能适得其反。如果内联增加了调用函数(calling function)的体积,它会降低调用者的“引用局部性(locality of reference)”(译注:参见[Meyers96]条款18);这意味着,如果调用者的内部指令循环(inner loop)不再和处理器高速缓存的大小相匹配,整个程序的执行速度实际上会降低。
不要忘记:客观地说,大多数程序的速度并非受限于CPU。最常见的瓶颈可能在于I/O上的限制,这包括很多方面,如网络带宽或延迟、对文件或数据库的访问,等等。
d) 开发速度和编译时间。为了得到最有效的利用,被内联的代码必须对调用者可见;这意味着,调用者必须依赖于被内联代码的内部细节。依赖另一个模块的内部实现细节必然增加模块的实际耦合性(但不会增加理论耦合性,因为调用者实际上没有使用被调用者的任何内部实现。)通常情况下,当普通函数被修改时,调用者无需重新编译,只需重新链接。当内联函数被修改时,调用者必须重新编译。还有,内联函数本身会在调试时期单独影响开发速度,因为,要想单步跟踪到内联函数的内部,或者在内联函数内部管理断点,对大多数调试器来说会更困难。
有一种情况下,一些人会认为内联是对开发速度的一种优化(这一点有一些争议)——为了避免让数据成员为公有成员,提供一个存取函数(accessor,见后)是好的做
法,但写这样一个存取函数的代价可能很高。这种情况下,一些人会认为,使用内联会带来好的编码风格和更好的模块独立性。
最后请记住,如果你想用什么方式提高效率,总是先借助你的算法和数据结构。它们会给你的程序带来数量级的整体改善,而内联之类的过程优化(process optimization)通常(注意,“通常”)收效甚微。
不妨说“现在不行”
3. 何时应当决定使用内联函数?如何决定?
和使用其它任何一种优化技术一样,答案是:在分析工具告诉你这样做之前,不要贸然行事。这条原则有几个合理的例外——在有些场合下你可以毫不迟疑地内联一个函数:例如空函数,而且会持续保持为空;或者,你非得这么做不可的时候——例如,在写一个非输出模板(non-exported template)的时候。
设计准则
使用优化的第一条原则:不要使用它。
使用优化的第二条原则:还是不要使用它。
结论:只要增加了耦合性,内联就总是会带来成本;绝对不要为某个东西事先支付成本,除非你知道它会带来好处——也就是说,回报大于支出。
“但我总能找到瓶颈在哪儿!”你会这样想。别着急,不是你一个人这样想。大多数程序员都或多或少地这样想过,但他们还是错了。完全错了。对于代码中真正的瓶颈所在,程序员是臭名昭著的猜测者。有时侯,我们撞大运似地蒙对了。但大多数时候,我们的猜测是错误的。
通常,只有实验数据(或称分析结果)可以帮助我们找出真正的热点所在。如果不借助某种分析工具,十有八九,一个程序员不可能识别出他(她)的代码中头号热点或瓶颈所在。干这行很多年了,我曾遇到过几个反对这一事实的程序员,他们(或他们的同事)坚持认为这一事实对他们不适用,声称自己一向能够“感觉到”自己代码中的热点所在。多年来,我从没有看到过这样的宣言变成一贯的事实。我们善于欺骗自己。
最后注意,使用这条准则的另一个实际原因是:对于哪一个内联函数不应该被内联,分析工具并不善于识别。
关于密集计算任务(例如数值计算程序库)
一些人要编写短小紧凑的程序库代码,例如高级科学和工程计算程序库,这些人有时可以凭直觉使用内联,并做得很不错。然而,即使是这些程序员,他们也倾向于明智地使用内联,认为优化宜迟不宜早。注意,写一个模块,然后通过“打开内联”和“关闭内联”的方式来比较性能,这通常不是一个好主意,因为“全开”和“全闭”是一种很粗糙的分析方法,它只能告诉你一般情况。它不会告诉你哪个函数受益,也不会告诉你每个函数受益多少。即使在这些情况下,你也应该使用分析工具并基于它的建议去优化。
关于存取函数
会有一些人争辩说:只有一行代码的存取函数(例如“X& Y::f() { return myX_; }”)是个合理的例外,它“可以”或“应该” 被自动内联。我知道这样做的道理,但要小心行事。不管怎么说,所有被内联的代码都会增加耦合性。所以,除非你事先确信内联会带来好处,否则,将使用内联的决定延迟到分析之后是没有坏处的。到了那时,如果分析工具真的指出内联会有好处,你至少知道,你正在做的是值得做的事;而且,你也将耦合和可能的编译开销延迟了——延迟到你确实知道内联真的有必要的时候。这样做不会让你吃亏的,真的!
设计准则
在性能分析证明确实必要之前,避免内联或详细优化。