2.4不要进行不成熟的优化
摘要
拉丁谚语云,快马无需鞭策:不成熟优化的诱惑非常大,而它的无效性也同样严重。优化的第一原则就是:不要优化。优化的第二原则(仅适用于专家)是:还是不要优化。再三测试,而后优化。
讨论
正如[Stroustrup00]§6开始所引用的优美名言说的那样:
不成熟的优化是万恶之源。——Donald Knuth (引用Hoare的话)
另一方面,我们不能忽视效率。——Jon Bentley
Hoare和Knuth当然而且永远是完全正确的(见第6条和本条)。Bentley亦然(见第9条)。
我们将不成熟的优化定义为这样的行为:以性能为名,使设计或代码更加复杂,从而导致可读性更差,但是并没有经过验证的性能需求(比如实际的度量数据和与目标的比较结果)作为正当理由,因此本质上对程序没有真正的好处。毫无必要而且无法度量的优化行为其实根本不能使程序运行得更快,这种情况简直是太常见了。
请永远记住:
让一个正确的程序更快速,
比让一个快速的程序正确,要容易得太多、太多。
因此,默认时,不要把注意力集中在如何使代码更快上;首先关注的应该是使代码尽可能地清晰和易读(见第6条)。清晰的代码更容易正确编写,更容易理解,更容易重构——当然也更容易优化。使事情复杂的行为,包括优化,总是以后再进行的——而且只在必要的时候进行。
不成熟的优化经常并不能使程序更快,这主要有两方面原因。一方面,我们程序员在估计哪些代码应该更快或者更小,以及代码中哪里会成为瓶颈上名声很臭。包括本书的作者,也包括读者你。考虑一下这些事实吧:现代计算机都具有极为复杂的计算模型,经常是几个流水线处理单元并行工作,深高速缓存层次结构,猜测执行(speculative execution)[3],分支预测……这还只是CPU芯片。在硬件之上,编译器也在尽其所能地猜测,将源代码转换为最能发掘硬件潜力的机器码。而在这些复杂的架构之上,还有……还有你——程序员的猜测。所以,如果只是猜测的话,你的那些目标不明确的微观优化就很难有机会显著地改善代码。因此,优化之前必须进行度量;而度量之前必须确定优化的目标。在需求得到验证之前,注意力应该放在头号优先的事情上——为人编写代码。(当有什么人要求你进行优化的时候,请进行需求验证。)
另一方面,在现代程序中,许多操作越来越不受CPU的限制。它们可能更受内存的限制、网络的限制、硬盘的限制,需要等待Web Service,或等待数据库。即使在最好的情况下,优化这些操作的应用程序代码,也只不过能使等待操作更快。这也意味着程序员浪费了宝贵的时间去改善没有必要改善的地方,却没有进行需要的有价值的改善。
当然,迟早有一天需要优化某些代码。到那时,首先要考虑算法优化(见第7条),并尝试将优化封装和模块化(比如,用一个函数或者类,见第5条和第11条),然后在注释中清楚地说明优化的原因并列出所用算法作为参考。
初学者常犯的一个错误,就是编写新代码时着迷于进行过度优化(而且充满自信),却牺牲了代码的可理解性。这常常会产生大杂烩代码,这种代码即使开始时是正确的,也非常难以阅读和修改。(见第6条。)
通过引用传递(见第25条),优先调用前缀形式的++和--(见第28条),和使用很自然地从指尖流出的惯用法,都不属于不成熟的优化。这些都不是不成熟的优化,而是在避免不成熟的劣化(见第9条)。
示例
例inline悖论。这个例子简单阐述了不成熟的微观优化所带来的隐性代价。分析器(profiler)能够通过函数的命中计数出色地告诉我们哪些函数应该但是没有标记为inline;然而,分析器在寻找哪些函数已经标记为inline但是不应该标记方面,却极不擅长。太多的程序员习惯以优化的名义“将inline作为默认选择”,这几乎总是以更高的耦合性为代价,而换来的好处到底如何却很可疑。(这里有一个前提,编写inline在所用的编译器上确实起作用。参阅[Sutter00]、[Sutter02]和[Sutter04]。)
例外情况
在编写程序库的时候,预测哪些操作最后会用于性能敏感的代码中更加困难。但即使是程序库的编写者,在实施容易令人糊涂的优化之前,也会对很大范围内的客户代码进行性能测试。
参考文献
[Bentley00] §6 ● [Cline99] §13.01-09 ● [Kernighan99] §7 ● [Lakos96] §9.1.14 ● [Meyers97] §33 ● [Murray93] §9.9-10, §9.13 ● [Stroustrup00] §6 introduction ● [Sutter00] §30, §46 ● [Sutter02] §12 ● [Sutter04] §25