优化过早是什么时候?

91
正如Knuth所说,
“我们应该忘记小的效率问题,大约97%的时间:过早优化是万恶之源。”
这是在Stack Overflow回答问题时经常出现的情况,比如“哪种循环机制最高效”,“SQL优化技巧?”(等等)。对于这些优化提示问题的标准答案是先对代码进行分析,看看是否存在问题,如果没有问题,那么你的新技术就是不必要的。
我的问题是,如果一种特定的技术不同但并不特别晦涩或隐晦,那真的可以被认为是过早优化吗?
这里有一篇相关文章,由Randall Hyde撰写,名为过早优化的谬论

46
很讽刺的是,许多人高喊着“过早优化是万恶之源”,却自己过早地优化了这句话。 - some
25
"我们应该忘记那些小的效率优化,大约97%的时间都是这样:过早地进行优化是万恶之源。然而,在关键的3%的情况下,我们不应放弃机会。"(唐纳德·克努斯) - some
2
我相信是CA Hoare说过这句话。甚至Knuth也这么说。 - jamesh
1
是的,Tony Hoare 最初说了“过早优化是万恶之源”的一部分,但 Knuth 引用/改编了他的话,并加上了后面的内容。 - nickf
2
虽然我同意这个引用经常被滥用和断章取义,但从定义上来说,它总是正确的,因为存在“过早优化”(但通常被错误地用作懒散设计和代码的辩解)。从定义上来说,如果优化发生在开发的最适时点,无论是在设计期还是其他任何时候,它都不是“过早”的。 - Lawrence Dol
显示剩余7条评论
20个回答

115

Donald Knuth发起了文学编程运动,因为他认为计算机代码最重要的功能是向人类读者传达程序员的意图。任何以性能为名义使您的代码难以理解的编码实践都是过早的优化。

一些被引入为了优化而产生的习惯用法变得非常流行,以至于每个人都能理解它们,它们已经不再是过早的优化了。例如:

  • 在C语言中使用指针算术运算而不是数组记号,包括使用这种惯用语的示例

    for (p = q; p < lim; p++)
    
    在Lua中重新绑定全局变量为局部变量,例如:
  • local table, io, string, math
        = table, io, string, math
    

在这些习语之外,冒险采用捷径可能会带来风险

除非

  • 程序运行缓慢(许多人忽略了这一点)。

  • 您已经有了一个测量数据(如分析报告等),表明该优化可以改善性能

(也可以为内存进行优化。)

直接回答问题:

  • 如果你的“不同”技术使程序难以理解,那么它就是一种过早的优化

编辑:针对评论,使用快速排序而不是插入排序之类的简单算法是每个人都理解和预期的另一个习惯用法。(尽管如果你编写自己的排序例程而不是使用库排序例程,希望你有一个非常好的理由。)


13
根据您的定义,如果一个快速排序算法实现难于理解和阅读,而冒泡排序相比之下更容易理解,那么这就是一种过早的优化。您不能针对内存进行优化吗?试着查找大型稀疏矩阵的同样例子。在我看来,大多数优化应该在设计阶段进行,也就是非常早期。 - SmacL
1
@frankodwyer:但是递增指针可能比递增计数器和使用数组符号更快,这可能是过早的优化。 - Joachim Sauer
5
@Norman: 尽管现在快速排序已经无处不在,但它在刚被发明时并非如此,因此可以得出结论,作者的过早优化实际上是没有任何必要的干扰,对吧? - Lawrence Dol
5
@Software Monkey:绝对不是这样。所有计算机科学研究都是浪费纳税人的钱,应该立即停止。 - Norman Ramsey
12
将包括您发明的任何排序算法在内的排序算法,写成一个名为 sortQuickly(...) 的独立函数,并加上适当的注释,便能使其更加清晰简明易懂。 - ilya n.
显示剩余5条评论

42

我的看法是,在设计阶段,应基于当前和更重要的未来需求进行90%的优化。如果您必须使用分析器,因为您的应用程序无法承受所需的负载,那么您已经太迟了,并且我认为这将浪费大量时间和精力,同时无法解决问题。

通常情况下,值得进行的唯一优化是在速度方面获得一个数量级的性能提升,或者在存储或带宽方面获得一个乘数。这些类型的优化通常涉及算法选择和存储策略,并且极难反转到现有代码中。它们可能甚至会影响您实现系统所用语言的决策。

因此,我的建议是根据您的需求而不是您的代码尽早优化,并考虑您的应用程序可能的延长寿命。


6
我不同意你的“太晚了”的结论。基本上,当一个假设不成立时,需要进行分析和剖析器需要告诉你哪个假设被打破。例如,在Java中,我发现针对StringBuffers的“删除位置0处的字符”在junit测试中运行良好,但对于大字符串非常慢。在剖析器准确定位到这段代码之前,我并没有怀疑它是罪魁祸首! - Thorbjørn Ravn Andersen
7
根据我的经验,我同意“当你需要优化工具时,往往为时已晚”。我的性能问题大多不是单一的瓶颈,而是分散在多个因素上。但是,由于我在低级代码和成本方面有很强的背景,我会本能地回避任何依赖于(显著重复)删除第一个字符的东西。 对“设计期间进行优化”表示支持。 - peterchen
@peterchen,只是出于好奇,对于“删除第一个字符串字符”,你会怎么做? - Ghos3t
1
@user258365:暴力方法是使用不需要为子字符串制作副本的字符串表示。对于不可变引用计数字符串来说,这几乎是微不足道的。另外,算法上的改变,比如替换(伪代码)while (s[0]==' ') s = s.substring(1) for(i=0; i<s.len && s[i]==' '; ++i); s=s.substring(i) --- 但这需要已经知道潜在的性能问题(分析器是不断学习的有价值工具)。 - peterchen
@ThorbjørnRavnAndersen,这里的重点是,如果您将性能视为功能需求之类的要求,并在设计阶段处理它,您可能永远不需要使用分析器。 您真的不应该需要编写任何代码来知道您的性能要求是否得到满足。 根据您的开头评论,我尝试将分析器视为调试器,因为我的大部分代码都是通过设计实现高效的,分析器用于异常情况。 并非总是如此,但这是计划的一部分 ;) - SmacL
显示剩余3条评论

32
如果您还没有进行性能分析,那么现在进行这样的操作就为时过早。

3
我支持其中的想法,但是:除非完全通过CPU周期来绑定实现,否则得到既可重现又可概括的测量结果就很困难 - 而且它越稳定,也就越不现实。 - peterchen
1
我对上面的答案有问题,因为它暗示着在编写代码之前无法优化算法。我的工作方式往往是设计算法以满足功能要求。查看它是否可能无法满足性能要求(例如高复杂度和可能会遇到大数据集),并在开始编码之前优化算法。优化只是为了达到最佳解决方案而进行的精细处理,通常在设计阶段效率最高。 - SmacL
2
我不同意。Knuth所说的是小效率问题。优化通常发生在设计阶段。它涉及选择适当的数据结构和算法,这些往往对性能有很大的影响,并且不能随意更换。 - haslersn
@haslersn:“Knuth谈论了小的效率问题” Donald Knuth:“许多当今软件工程师共享的常识是忽略小的效率问题;但我认为这只是对滥用的过度反应(...)在已经建立的工程学科中,一个容易获得的12%的改进从来不被视为边缘的(...)” - Pedro Amaral Couto

28
我的问题是,如果一种特定的技术不同但并不特别模糊或难懂,那么它真的可以被认为是过早地优化吗?
嗯...所以你有两种准备好的技术,成本相同(使用、阅读和修改的工作量相同),其中一种更有效率。在这种情况下,使用更有效率的技术不会被视为过早。
打断你的代码编写,寻找常见编程结构/库例程的替代方案,以防万一有更高效的版本存在,即使你并不知道你正在编写的相对速度实际上永远不会事实上重要... 那就是过早的优化。

3
如果你知道在特定场景下某个算法更加高效,那么就使用更高效的算法。如果你不知道最高效的算法,那么就先使用当前有的算法,并在之后进行性能分析以查看是否存在问题。 - grepsedawk

11

关于避免过早优化的整个概念,我看到的问题在于言行不一。

我做过很多性能调优,在本来设计良好的代码中挤出了大量的性能,似乎没有进行过过早的优化。 这里有一个例子。

几乎每种情况下,导致次优性能的原因是我称之为泛泛而谈的使用抽象的多层类和彻底面向对象的设计,而简单的概念将会更少(不如说是更加朴素),但完全足够。

而在教授这些抽象设计概念的教材上,例如通知驱动架构和信息隐藏,其中仅设置对象的布尔属性就可以产生无限级联效应的情况下,给出的理由是什么?效率

那么,这是过早优化还是不是呢?


我喜欢这个答案,因为它阐述了抽象和泛化的主要问题之一。当你将类层次结构泛化以支持更广泛的用例时,很容易严重影响最典型的用例的性能。同时,很容易附着于提供给定功能的类,而不检查该功能是否以可接受的性能水平提供给预期使用规模。 - SmacL
1
当简单的概念足以满足要求时,复杂的代码很少比简单的代码更优雅。(尽管我认为,如果有人试图在更复杂的情况下执行它,你必须确保你的简单代码实际上会爆炸,并清楚地指示不支持的状态/输入。) - jpmc26

9

首先,让代码正常运行。其次,验证代码正确性。第三,使其更快。

在第三阶段之前做任何代码更改都是过早的。我不确定如何对在此之前做出的设计选择进行分类(例如使用适合的数据结构),但我更喜欢使用易于编程的抽象化,而不是那些性能良好的,直到我可以开始使用分析和具有正确(虽然通常很慢)的参考实现来比较结果的阶段。


8
从数据库的角度来看,在设计阶段不考虑最佳设计是非常愚蠢的。数据库很难进行重构。一旦它们被设计得不好(这就是一个没有考虑优化的设计,无论你如何试图掩盖过早优化的荒谬之处),几乎无法从中恢复,因为数据库对整个系统的运作至关重要。正确地设计考虑到预期情况下的最佳代码比等到有一百万用户并且人们因为你在应用程序中使用游标而尖叫要便宜得多。其他优化,如使用可搜索代码、选择看起来最好的可能索引等,只有在设计时才有意义。快速而肮脏的原因是有原因的。因为它永远不能很好地工作,所以不要将快捷作为良好代码的替代品。此外,当您了解数据库性能调整时,您可以编写更有可能在相同时间内或更短时间内表现良好的代码,而不是编写性能不佳的代码。不花时间学习什么是良好的数据库设计,是开发人员的懒惰,而不是最佳实践。

7
您所讨论的似乎是优化,例如在进行大量密钥查找时使用基于哈希的查找容器(如哈希表)与索引容器(如数组)相比。这不是过早优化,而是应该在设计阶段决定的事情。
Knuth 规则所涉及的优化类型是最小化最常见代码路径的长度,通过重写汇编或简化代码来优化运行时间最长的代码等方式来实现。但是,这样做在确定需要这种优化的代码部分之前是没有用的,并且优化会使代码更难理解或维护,因此"过早优化是万恶之源"。
Knuth 还指出,与其优化,总是更好地改变程序使用的算法,即解决问题的方法。例如,虽然微调可以使程序的速度提高 10%,但根本上改变程序工作方式可能使其快 10 倍。
对于此问题发布的其他评论,算法选择并不等于优化。

6
这句话的意思是,通常情况下,优化是复杂而曲折的。作为架构师/设计师/程序员/维护人员,你需要清晰简明的代码来理解正在发生的事情。如果某个特定的优化是清晰简明的,可以尝试进行实验(但请回头检查该优化是否有效)。重点是在整个开发过程中保持代码的清晰简明,直到性能的好处超过编写和维护优化所带来的成本。

2
实际上,“优化”的很大一部分归结于选择适合工作的正确算法;这是一项高级活动,具有高级结果——与Knuth引用中的“小效率”相去甚远。 - Shog9

5
优化可以在不同的粒度级别上进行,从非常高级别到非常低级别:
1. 从良好的架构、松散耦合、模块化等方面入手。 2. 为问题选择正确的数据结构和算法。 3. 优化内存,尝试将更多的代码/数据放入缓存中。内存子系统比CPU慢10到100倍,如果您的数据被分页到磁盘上,则慢1000到10000倍。谨慎考虑内存消耗更有可能提供重大收益,而不是优化单个指令。 4. 在每个函数内,适当使用流控制语句。(将不可变表达式移动到循环体外。在switch/case中首先放置最常见的值,等等。) 5. 在每个语句内,使用产生正确结果的最有效表达式。(乘法与移位等)
挑剔是否使用除法表达式或移位表达式并不一定是过早的优化。只有在优化架构、数据结构、算法、内存占用和流控制之前这样做才算是过早。
当然,如果没有定义目标性能阈值,则任何优化都是过早的。
在大多数情况下,要么:
A)通过执行高级别优化,您可以达到目标性能阈值,因此不需要调整表达式。
要么
B)即使执行了所有可能的优化,您仍无法满足目标性能阈值,并且低级别优化在性能上的差异不足以证明可读性的损失。
根据我的经验,大多数优化问题可以在架构/设计或数据结构/算法级别上解决。通常(尽管并非总是如此),需要优化内存占用。但是很少需要优化流控制和表达式逻辑。而在那些实际上需要这样做的情况下,这往往是不够的。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接