优化何时过早?


82

正如克努斯所说,

我们应该忘记效率低下的问题,例如大约97%的时间:过早的优化是万恶之源。

这是Stack Overflow经常回答诸如“哪种是最有效的循环机制”,“ SQL优化技术”之类的问题。(依此类推)。这些优化技巧问题的标准答案是分析您的代码,首先查看是否有问题,如果不是,那么就不需要您的新技术了。

我的问题是,如果特定技术有所不同,但不是特别晦涩难懂,那真的可以认为是过早的优化吗?

这是Randall Hyde的相关文章,称为《过早优化的谬误》


41
这是一种讽刺的是,许多人谁大喊“过早的优化是所有罪恶的根源”自己过早地优化报价:(续)
一些

21
“我们应该忘记小的效率,大概说的97%的时间:过早的优化是所有罪恶的根源然而,我们不应该在关键的3%通过了我们的机会。”(高德纳)
一些

2
我相信是CA Hoare这么说的。甚至Knuth也这样说。
jamesh

1
是的,托尼·霍尔(Tony Hoare)首先说过“过早的优化是所有邪恶部分的根源”,但我相信Knuth引用/改写了他的内容,其余内容,我相信
尼克

2
我同意报价是最常滥用和脱离上下文的问题,但从定义上讲,由于“过早”,它始终是正确的(但是,它经常不正确地用作草率的设计和代码的理由)。根据定义,如果优化发生在开发中最合适的时间点,无论是在设计过程中还是在其他任何时候,它都不是“过早的”。
劳伦斯·多尔

Answers:


103

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

以优化的名义引入的某些习惯用法已经变得如此流行,以至于每个人都理解它们,并且它们已经成为期望的,而不是过早的。例子包括

  • 在C语言中使用指针算术代替数组符号,包括使用如下惯用法

    for (p = q; p < lim; p++)
    
  • 在Lua中将全局变量重新绑定到局部变量,如

    local table, io, string, math
        = table, io, string, math
    

除了这些惯用语之外,捷径还招致您的危险

所有优化都为时过早,除非

  • 程序太慢(许多人忘记了这一部分)。

  • 您有一个度量(概要文件或类似文件)显示优化可以改善性能

(也可以优化内存。)

直接回答问题:

  • 如果您的“不同”技术使程序难以理解,那么这是过早的优化

编辑:为了回应评论,使用快速排序而不是像插入排序这样的更简单算法每个人都能理解和期望的习惯用法的另一个示例。(虽然如果你写你自己的排序例程,而不是使用库排序例程,一个希望你有一个非常好的理由。)


13
根据您的定义;如果快速排序的实现比冒泡排序更难于阅读和理解,那是过早的优化。您无法优化内存?尝试查找大型稀疏矩阵的相同示例。恕我直言,大多数优化应该在设计阶段进行。我,很早。
SmacL

1
@frankodwyer:但是增加指针可能比增加计数器和使用数组表示法更快,这将是过早的优化。
约阿希姆·绍尔

5
@Norman:尽管quicksort现在无处不在,但它并不是第一次发明的,因此QED是一个过早的优化,作者没有任何麻烦,对吗?
劳伦斯·多尔

5
@软件猴子:绝对。所有CS研究都浪费了纳税人的钱,应立即停止。
Norman Ramsey

11
如果将排序算法编写为带有适当注释的称为sortQuickly(...)的单独函数,则任何排序算法(包括您发明的算法)都是清晰明了的。
ilya n。

40

恕我直言,您的优化的90%应该在设计阶段根据既定的当前需求(更重要的是未来的需求)进行。如果由于应用程序无法扩展到所需的负载而不得不取出探查器,那么您将它留得太晚了,IMO将浪费大量的时间和精力,而无法纠正问题。

通常,唯一值得进行的优化是那些可以在速度方面提高一个数量级的性能,或者在存储或带宽方面提高一个倍数的优化。这些类型的优化通常与算法选择和存储策略有关,并且很难转换为现有代码。它们可能会影响您对系统语言的决策。

因此,我的建议是根据您的要求(而不是代码)及早进行优化,并考虑延长应用程序的使用寿命。


6
我不同意您“为时已晚”的结论。当假设不成立时,基本上需要进行概要分析,并且需要使用探查器来告诉您假设破裂的原因。例如,我发现Java中的StringBuffers的“删除位置0处的字符”在junit测试中效果很好,但是对于大字符串则非常慢。在探查器将其确定为罪魁祸首之前,我没有怀疑该代码!
托尔比约恩Ravn的安徒生

7
根据我的经验,我确实同意“当您需要探查器时,已经很晚了” –我的大部分性能问题都不是单一的瓶颈,而是分布在多个贡献者上。但是,然后,我在低级代码和成本方面有很强的背景,并且本能地避免了任何依赖于(显着重复)删除第一个字符串字符的行为。+1表示“在设计期间进行优化”。
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,我曾作为顾问帮助团队完成项目,但是由于未计划严重的性能问题(除了意大利面条代码),这是不可能的。应该显示所有患者病历的时间顺序图。对整个数据只发出了一个请求,例如Google Maps提取整个世界。开发错误的代码,期望稍后进行概要分析,使项目失败。
Pedro Amaral Couto

30

如果尚未进行概要分析,则为时过早。


3
我同意其背后的想法,但也要同意:除非实现完全受CPU周期的约束,否则很难获得一种既可重复又可以推广的度量,而且度量越稳定,其现实性也就越差。
peterchen

1
我对以上答案的问题是,这意味着您无法在编码之前优化算法。我的工作方式往往是设计算法来满足功能要求。在开始编写代码之前,请检查是否有可能无法满足性能要求(例如,复杂性高且可能击中大型数据集)并优化算法。优化只是为了达到最佳解决方案而进行的优化,通常在设计阶段最有效地完成。
SmacL

2
我不同意 Knuth在谈论效率低下的问题。优化通常在设计阶段进行。它涉及选择适当的数据结构和算法,这些数据结构和算法通常会对性能产生重大影响,因此以后不必交换。
haslersn

@haslersn:“ Knuth谈论的是小效率”,Donald Knuth:“当今许多软件工程师的传统观念都要求忽略小效率;但是我相信这仅仅是对滥用行为的过度反应(...)在建立工程学科提高了12%,很容易获得,是从来没有考虑边际(...)”
佩德罗·阿马拉尔·库托

27

我的问题是,如果特定技术有所不同,但不是特别晦涩难懂,那真的可以认为是过早的优化吗?

嗯...所以您手头准备了两种技术,它们的成本相同(使用,阅读,修改的工作量相同),而一种效率更高。不,在这种情况下,使用效率更高的工具可能还为时过早。

中断代码编写以寻找通用编程结构/库例程的替代方案,这是偶然的机会,尽管有一个众所周知的事实,相对而言,您所编写的相对速度永远不会有任何影响,但仍有一个更高效的版本挂在某个地方。 ..这是不成熟的。


3
同意,如果您知道一种算法对您的用例更有效,则一定要使用效率更高的一种。如果您不知道最有效的算法,请稍后使用所拥有的内容和配置文件来查看是否有问题。
grepsedawk

10

我在避免过早优化的整个概念中遇到了这个问题。

说和做之间存在脱节。

我已经进行了许多性能调整,从原本精心设计的代码中挤出了很多因素,似乎没有进行过早的优化。 这是一个例子。

在几乎每种情况下,性能欠佳的原因都是我所说的疾驰一般性,即使用抽象多层类和透彻的面向对象设计,其中简单的概念不太优雅,但完全足够。

在教授这些抽象设计概念的教材中(例如通知驱动的体系结构)和信息隐藏中,仅设置对象的布尔属性可以对活动产生无限的连锁反应,给出的原因是什么?效率

那么,是不是过早的优化呢?


我喜欢这个答案,因为它说明了抽象和泛化的主要问题之一。在概括类层次结构以支持更广泛的用例时,很容易严重损害最典型用例的性能。也很容易锁定到提供给定功能的类上,而无需检查功能是否在预期使用范围内以可接受的性能水平提供。
SmacL 2010年

1
“简单的概念不太优雅,但完全足够了”当简单代码满足要求时,复杂代码很少比简单代码更优雅。(尽管,我认为您必须确保如果有人尝试在更复杂的情况下执行,则您的简单代码实际上会爆炸,并明确指出了不受支持的状态/输入。)
jpmc26

8

首先,使代码正常工作。其次,验证代码是否正确。第三,加快速度。

在阶段3之前完成的任何代码更改绝对为时过早。我不确定如何对之前做出的设计选择进行分类(例如使用合适的数据结构),但是我更倾向于转向使用易于编程的抽象方法,而不是性能良好的方法,直到我在这个阶段,我可以开始使用概要分析,并使用正确的(尽管通常很慢)参考实现与结果进行比较。


7

您似乎在谈论的是优化,例如使用基于散列的查找容器,而不是像索引数组那样使用索引容器,这样可以完成许多关键查找。这不是过早的优化,而是在设计阶段应决定的事情。

Knuth规则所涉及的优化类型是最小化最常见的代码路径的长度,例如通过重写汇编或简化代码来优化运行最频繁的代码,从而使其通用性降低。但是,这样做是没有用的,除非您确定代码的哪些部分需要这种优化,并且优化会(可能?)使代码更难于理解或维护,因此“过早的优化是万恶之源”。

Knuth还说,改变而不是优化总是可以改变程序使用的算法以及解决问题的方法。例如,尽管进行一些微调可能会使您的优化速度提高10%,但从根本上改变程序的工作方式可能会使它的速度提高10倍。

对此问题发表了很多其他评论:算法选择!=优化


7

从数据库的角度来看,在设计阶段最好不要考虑最佳设计。数据库不容易重构。一旦设计不当(无论您如何尝试隐藏过早的优化,这都是不考虑优化的设计),因为数据库对于整个系统的运作。正确地设计出适合您期望情况的最佳代码,要比等到有一百万用户和人们在整个应用程序中使用游标而大喊大叫,所花费的成本要少得多。其他优化(例如使用可编辑代码,选择看起来最好的索引等)仅在设计时才有意义。之所以称其为快速而肮脏,是有原因的。因为它永远无法正常工作,所以不要用速度来代替好的代码。坦率地说,当您了解数据库中的性能调整时,您可以编写在同一时间或更短时间内表现良好的代码,而不是编写性能不佳的代码所需要的时间。不花时间去学习什么是性能好的数据库设计,这是开发人员的惰性,而不是最佳实践。


6

格言的重点是,优化通常是复杂而复杂的。而通常,你建筑师/设计师/程序员/维护需要,以了解什么是要去明确和简洁的代码。

如果特定的优化清晰明了,请随时尝试(但请返回并检查该优化是否有效)。关键是要在整个开发过程中保持代码的简洁明了,直到性能的好处超过编写和维护优化所引起的成本。


2
实际上,很多“优化”归结为为工作选择合适的算法。这是一项具有高水平成果的高水平活动,与Knuth报价中的“低效率”相去甚远。
Shog9

4

我尝试仅在确认性能问题时进行优化。

我对过早优化的定义是“将精力浪费在未知的性能问题上”。最肯定有一个优化的时间和地点。但是,诀窍是仅在影响应用程序性能并且超出性能影响的地方才花费额外的成本。

在编写代码(或数据库查询)时,我努力编写“高效”代码(即以合理的最简单逻辑快速,完全地执行其预期功能的代码。)请注意,“高效”代码不一定与“优​​化”代码相同码。优化通常会在代码中引入额外的复杂性,从而增加该代码的开发和维护成本。

我的建议:只有在您可以量化收益时,才尝试支付优化的费用。


4

编程时,许多参数至关重要。其中包括:

  • 可读性
  • 可维护性
  • 复杂
  • 坚固性
  • 正确性
  • 性能
  • 开发时间

优化(追求性能)通常以牺牲其他参数为代价,并且必须与这些方面的“损失”相平衡。

当您选择性能良好的知名算法时,通常可以接受“优化”前期成本。


1
您上面的列表中缺少最重要的质量检查参数。满足要求。如果某个软件不符合目标受众的要求,则所有其他参数都是没有意义的。如果性能不可接受,则说明未满足要求。
SmacL

3
可以说这是正确的。此外,在需求中很少有“尽可能快”的“性能”,甚至Ola认为要与其他需求进行权衡仍然是正确的。
frankodwyer

4

优化可以在从非常高级到非常低级别的不同粒度级别上进行:

  1. 从良好的架构,松散的耦合,模块化等开始。

  2. 为问题选择正确的数据结构和算法。

  3. 优化内存,尝试在缓存中容纳更多代码/数据。内存子系统比CPU慢10到100倍,并且如果将数据分页到磁盘,则它要慢1000到10,000倍。与优化单个指令相比,谨慎使用内存更有可能带来重大收益。

  4. 在每个函数中,适当使用流控制语句。(将不可变的表达式移到循环体的外部。将最常用的值放在开关/大小写中,等等)。

  5. 在每个语句中,使用最有效的表达式产生正确的结果。(乘以vs.移位等)

关于是否使用除法表达式或移位表达式的挑剔未必是过早的优化。如果您没有先优化体系结构,数据结构,算法,内存占用和流控制,那么这样做还为时过早。

当然,如果您没有定义目标绩效阈值,那么任何优化都为时过早。

在大多数情况下,可以:

A)您可以通过执行高级优化来达到目标​​性能阈值,因此无需摆弄表达式。

要么

B)即使执行了所有可能的优化之后,您也不会达到目标性能阈值,并且低级优化不会在性能上产生足够的差异以证明可读性下降。

以我的经验,大多数优化问题都可以在体系结构/设计或数据结构/算法级别上解决。经常(尽管并非总是)需要优化内存占用量。但是很少需要优化流控制和表达逻辑。并且在那些实际上有必要的情况下,它很少是足够的。


3

在极端情况下,应保留使用分析器的需求。项目的工程师应该知道性能瓶颈在哪里。

我认为“过早的优化”是非常主观的。

如果我正在编写一些代码,并且知道应该使用哈希表,那么我将这样做。我不会以某种有缺陷的方式实施它,然后等到有人遇到问题时,等一个月或一年后再提交错误报告。

与从一开始就以明显的方式优化设计相比,重新设计的成本更高。

显然,有些小事情会在第一时间被遗漏,但是这些很少是关键的设计决策。

因此:IMO本身就是代码的味道,而不是优化设计。


问题是瓶颈常常出现在您从未认为会成为问题的代码段中。配置文件省去了伪装,并显示了程序的实际成本中心。最好从一开始就做显而易见的事情,但是对于其他所有事情,都有分析。
克里斯·史密斯,

2

诺曼的答案非常好。不知何故,您通常会进行一些“过早优化”,实际上这是最佳实践,因为这样做通常是效率低下的。

例如,要添加到Norman的列表中:

  • 在Java(或C#等)中使用StringBuilder串联,而不是String + String(在循环中);
  • 避免像这样在C中循环:(for (i = 0; i < strlen(str); i++)因为strlen在这里是一个函数调用,每次在字符串上遍历,在每个循环上调用);
  • 在大多数JavaScript实现中,这样做似乎更快,for (i = 0 l = str.length; i < l; i++)并且仍然可读,所以可以。

等等。但是,这种微优化绝不能以代码的可读性为代价。


2

值得注意的是,Knuth的原始报价来自他撰写的一篇论文,该论文提倡goto在精心挑选和测量的区域使用,以消除热点。他的报价是一个警告,他补充说,以证明他的使用理由goto以加速这些关键循环。

再次,如果例如n的平均值约为20,并且如果在程序中执行了大约一百万次搜索例程,则可以显着节省总体运行速度。这样的循环优化[使用gotos]并不难学习,正如我已经说过的那样,它们仅适用于程序的一小部分,但通常可以节省大量成本。[...]

并继续:

当今许多软件工程师的共识是要求忽略小型计算机的效率。但是我相信这仅仅是对那些精打细算的愚蠢程序员的过度反应,这些程序员无法调试或维护其“优化”程序。在既定的工程学科中,容易实现的12%的改善从未被视为微不足道;而且我认为在软件工程中应采用相同的观点。当然,我不会在单发工作上进行这样的优化,但是当要准备高质量的程序时,我不想将自己限制在拒绝我如此高效的工具上(例如,goto 在这种情况下的陈述)。

请记住他是如何在报价中使用“优化”的(该软件实际上可能效率不高)。还请注意,他不仅批评这些“细心而笨拙的”程序员,而且还批评那些建议您应该始终忽略低效率的人。最后,到经常引用的部分:

毫无疑问,提高效率会导致滥用。程序员浪费大量时间来考虑或担心程序非关键部分的速度,而在考虑调试和维护时,这些提高效率的尝试实际上会产生严重的负面影响。我们应该忘记效率低下的情况,例如97%的时间;过早的优化是万恶之源。

...然后再介绍一下分析工具的重要性:

对程序的哪些部分真正关键进行先验判断通常是一个错误,因为使用测量工具的程序员的普遍经验是他们的直观猜测会失败。在使用此类工具七年后,我深信从现在开始编写的所有编译器都应设计为向所有程序员提供反馈,表明他们的程序哪些部分花费最大。实际上,除非已将其专门关闭,否则应自动提供此反馈。

人们到处都在滥用他的报价,常常暗示当他的整个论文都提倡微优化时,微优化还为时过早!他批评的一群人赞同这种“传统智慧”,因为他总是忽略小规模的效率,而经常误以为是他的报价,最初的报价部分是针对那些不鼓励所有形式的微观优化的人。

然而,当有经验的手拿着轮廓仪使用时,这是对适当应用微优化的支持。如今,类似的类比可能像是:“人们不应该盲目地优化软件,但是自定义内存分配器在关键区域应用以改善引用的局部性时可以发挥巨大作用,”或“使用因此,SoA rep确实很难维护,您不应该在所有地方都使用它,但是如果由经验丰富的指导者适当地应用它,它可以更快地消耗内存。

每当您尝试像Knuth所提倡的那样推广精心应用的微优化时,最好放弃免责声明,以阻止新手过于兴奋和盲目地追求优化,例如重写其整个软件以供使用goto。这部分是他在做什么。他的报价实际上是免责声明的一部分,就像有人骑摩托车跳过燃烧的火坑可能会添加免责声明,即业余爱好者不应该在家中尝试此操作,同时批评那些没有适当知识和设备而受伤的人。

他认为“过早的优化”是指那些实际上不知道自己在做什么的人所进行的优化:不知道优化是否确实需要,没有使用适当的工具进行测量,也许不了解其本质。他们的编译器或计算机体系结构,尤其是“笨拙的,笨拙的”,这意味着他们忽略了尝试捏造便士来优化(节省数百万美元)的巨大机会,而在创建代码时却无能为力更有效地调试和维护。

如果您不适合“ pennywise-and-pound-愚蠢”类别,那么即使您使用agoto来加快关键循环的速度,也不会按照Knuth的标准过早地进行优化(这不太可能对当今的优化器有很大帮助,但是如果这样做的话,并且在真正关键的领域,您就不会过早地进行优化)。如果您实际上在真正需要的领域中应用您所做的任何工作,而他们确实从中受益,那么在Knuth的眼中,您所做的就很棒。


1

对我来说,过早的优化意味着在拥有一个正常工作的系统之前以及在实际剖析代码并了解瓶颈所在之前,尝试提高代码的效率。即使在那之后,在许多情况下,可读性和可维护性也应该在优化之前实现。


1

我认为公认的最佳实践不是过早的优化。取决于可能的性能问题(取决于使用场景)的假设时间,更多的是消耗时间。一个很好的例子:如果您花了一个星期试图优化对象的反射,然后再证明它是瓶颈,那么您就过早优化了。


1

除非由于用户或业务需求而发现需要从应用程序中获得更高的性能,否则没有理由担心优化。即使这样,在配置好代码之前也不要做任何事情。然后攻击耗时最多的零件。


0

我的看法是,如果您在不知道在不同情况下可以获得多少性能的情况下进行优化,那是过早的优化。代码的目标实际上应该使人类最容易阅读。


0

正如我在类似问题上发布的那样,优化规则是:

1)不要优化

2)(仅适用于专家)稍后进行优化

优化何时过早?通常。

例外可能是在您的设计中,或者是在大量使用的封装良好的代码中。过去,我研究了一些时间紧迫的代码(RSA实现),其中查看了编译器生成的汇编器并在内部循环中删除了一条不必要的指令,从而使速度提高了30%。但是,使用更复杂的算法所带来的速度却要高出几个数量级。

优化时要问自己的另一个问题是“我是否相当于在这里优化300波特调制解调器?” 。换句话说,摩尔定律会使您的优化在不久之后就变得无关紧要了。仅通过在问题上投入更多硬件就可以解决许多扩展问题。

最后但并非最不重要的一点是,在程序运行太慢之前进行优化还为时过早。如果您正在谈论的是Web应用程序,则可以在负载下运行它以查看瓶颈所在-但可能是您将遇到与大多数其他网站相同的扩展问题,并且将采用相同的解决方案。

编辑:顺便说一句,关于链接的文章,我会质疑许多假设。首先,摩尔定律在90年代停止运行并不是真的。其次,用户的时间比程序员的时间更有价值并不明显。大多数用户(至少可以说)并不是疯狂地使用每个可用的CPU周期,他们可能正在等待网络做某事。另外,当程序员的时间从实施其他事情转移到将用户在电话上时程序所做的工作减少几毫秒的时间时,这会带来机会成本。更长的时间通常不被优化,这是错误修复。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.