过早的优化真的是万恶之源吗?


215

我的一位同事今天提交了一个名为的类ThreadLocalFormat,该类基本上将Java Format类的实例移动到了线程本地中,因为它们不是线程安全的,而且创建起来“相对昂贵”。我写了一个快速测试,计算出我每秒可以创建200,000个实例,问他是否创建了那么多实例,对此他回答“远不及那么多”。他是一位出色的程序员,并且团队中的每个人都非常熟练,因此我们对所生成的代码毫无疑问,但这显然是在没有实际需要的情况下进行优化的情况。他应我的要求取消了代码。你怎么看?这是“过早优化”的案例吗,真的有多糟糕?


22
我认为您需要区分过早的优化和不必要的优化。对我来说,过早暗示“生命周期太早”,而不必要则暗示“不会增加重大价值”。国际海事组织,对后期优化的要求意味着劣质设计。

109
是的,但是邪恶是一个多项式并且有很多根,其中一些是复杂的。
dan_waterworth 2011年

7
您应该考虑,Knuth编写了这本1974年。在七十年代,编写慢速程序并不像现在这样容易。他在编写Pascal时就牢记于心,而不是Java或PHP。
2013年

4
不。所有邪恶的根源在于贪婪。
图兰斯·科尔多瓦

12
@ceving在70年代,编写慢速程序就像今天一样容易。如果选择错误的算法或错误的数据结构,则BAM!到处都有糟糕的表现。有人可能反驳。如今,这是更多的工具,对于程序员仍然编写受最基本的保存操作影响的软件而言,这是不可原谅的。并行几乎成为一种商品,我们仍然受苦。不能将性能下降归咎于语言或工具,CPU或内存。这是许多事情之间的微妙平衡,这就是为什么几乎不可能尽早优化的原因。
Alex

Answers:


322

记住完整的报价很重要:

我们应该忘记效率低下的问题,例如大约97%的时间:过早的优化是万恶之源。然而,我们不应放弃我们那临界的3%的机会。

这意味着,在没有可衡量的性能问题的情况下,您不应优化,因为您认为自己将获得性能上的提升。有明显的优化(例如,没有在紧密的循环内进行字符串连接),但是在进行测量之前,应避免那些并非显而易见的优化。

“过早的优化”最大的问题是,它可能会引入意外的错误,并且可能会浪费大量时间。


7
来自唐纳德·克努斯(Donald Knuth),如果他有证据支持我,我不会感到惊讶。BTW,Src:“转到语句进行结构化编程”,ACM Journal Computing Surveys,第6卷,第4期,1974年12月,第268页。citeseerx.ist.psu.edu/viewdoc/...
mctylr

28
... 一个好的程序员不会因这种推理而沾沾自喜,他应该明智地仔细看待关键代码;但只有在确定了该代码之后(其余的引号)
mctylr 2010年

21
我今天有一个2万名代表用户,告诉我使用a HashSet代替a List是过早的优化。有问题的用例是一个静态初始化的集合,其唯一目的是用作查找表。我认为在为工作选择正确的工具与过早的优化之间存在区别是没有错的。我认为您的帖子证实了这一理念:There are obvious optimizations...anything that isn't trivially clear optimization should be avoided until it can be measured.HashSet的优化已得到全面衡量和记录。
暗恋

9
@crush:是的:Set在语义上也比更具正确性和信息性List,因此它在优化方面还不止于此。
Erik Allik

7
我想补充一点,不要将过早的优化与将整个应用程序体系结构设计为总体上可以快速运行,可扩展并易于优化而混淆。
Erik Allik 2014年

111

过早的优化是万恶之源,因为微优化忽略了上下文。他们几乎永远不会表现出预期的行为。

有哪些重要的早期优化按重要性排序:

  • 架构优化(应用程序结构,其组成和分层的方式)
  • 数据流优化(应用程序内部和外部)

一些中期开发周期优化:

  • 数据结构,引入新的数据结构,这些数据结构在必要时具有更好的性能或更低的开销
  • 算法(现在是开始在quicksort3和heapsort之间做出决定的好时机了;)

一些最终开发周期的优化

  • 查找代码热点(紧密循环,应该对其进行优化)
  • 基于分析的代码计算部分的优化
  • 现在可以在应用程序上下文中完成微优化,并且可以正确衡量其影响。

并非所有的早期优化都是有害的,如果在开发生命周期中的错误时间进行微优化,则是有害的,因为它们可能会对体系结构产生负面影响,对初始生产力产生负面影响,在性能上不相关甚至最终会产生不利影响。因不同的环境条件而发展。

如果性能是令人关注的(并且应该一直如此),那就要大加考虑。性能是一幅大图,与诸如以下内容无关:我应该使用int还是long?在使用性能时,请选择“自上而下”,而不是“ 自下而上”


约瑟夫·纽科默(Joseph M. Newcomer)撰写的“优化:您最糟糕的敌人”:flounder.com/optimization.htm
罗恩·卢布

53

没有先进行测量就进行优化几乎总是过早的。

我认为在这种情况下是正确的,在一般情况下也是如此。


这儿这儿!未经考虑的优化使代码无法维护,并且常常是导致性能问题的原因。例如,您对程序进行多线程处理是因为您认为它可能会提高性能,但是,真正的解决方案应该是多个流程,而现在这些流程实在太复杂了,难以实现。
詹姆斯·安德森

除非有文件记载。
nawfal 2014年

是。完全同意。首先必须对其进行测量。直到测试完所有东西并衡量每个步骤,您才能知道瓶颈在哪里。
奥利弗·沃特金斯

测量可能会说谎。我已经看到经验丰富的专家花了数周时间阅读痕迹和运行配置文件,以撞墙,他们认为再也没有收获。然后,我阅读了整个代码,并在几个小时内进行了几处全面的更改,使性能提高了10倍。概要文件未显示热路径,因为整个代码设计不良。我还看到剖析器要求在不应有的路径上使用热路径。一个人“测量”本来可以优化热路径,但是他们应该意识到热路径是其他不良代码的症状。
Bengie

42

如果优化导致以下情况,则是“邪恶的”:

  • 不太清楚的代码
  • 明显更多的代码
  • 不太安全的代码
  • 浪费程序员时间

在您的情况下,似乎已经花了一些程序员时间,代码并不太复杂(从您的评论中猜测,团队中的每个人都可以理解),并且该代码更能证明未来(成为如果我理解您的描述,现在就可以安全处理线程)。听起来只有一点邪恶。:)


4
仅当成本(按项目要点计算)大于交付的摊销价值时。复杂性通常会带来价值,在这种情况下,人们可以封装价值,使其通过您的标准。它还可以重复使用,并继续提供更多价值。

1
前两点是我的主要观点,第四点是进行过早优化的负面结果。特别是,每当我看到有人从标准库中重新实现功能时,它都是一个危险信号。就像,我曾经看到有人实现用于字符串操作的自定义例程,因为他担心内置命令太慢。
2011年

8
使代码线程安全不是优化。
mattnz

38

我很惊讶这个问题已有5年的历史了,但没有人发表过Knuth要说的话,而没有几个句子。围绕着名言的几段对其进行了很好的解释。被引用的论文被称为“ 带有转到语句的结构化编程 ”,尽管它已有近40年的历史,但它涉及的争议和软件运动已不复存在,并且具有许多人从未使用过的编程语言示例听说,它所说的内容仍然惊人地多。

这是一个更大的报价(来自pdf的第8页,原始的第268页):

从示例2到示例2a的速度改进仅约12%,许多人认为这是微不足道的。当今许多软件工程师的共识是要求忽略小型服务器的效率。但是我相信这只是对他们看到的精打细算的愚蠢行为的过度反应,这些精明的愚蠢而笨拙的程序员无法调试或维护其“优化”程序。在既定的工程学科中,容易实现的12%的改善从未被视为微不足道;我相信在软件工程中应该有同样的观点。当然,我不会一劳永逸地进行这样的优化,但是当要准备高质量程序时,我不想将自己限制在那些拒绝我如此高效的工具上。

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

然而,我们不应放弃我们那临界的3%的机会。一个好的程序员不会因这种推理而沾沾自喜,他应该明智地仔细看待关键代码。但只有在识别出该代码之后。对程序的哪些部分真正重要进行先验判断通常是一个错误,因为使用测量工具的程序员的普遍经验是他们的直观猜测会失败。

前一页的另一个优点:

当然,在过去的十年中,根据时代的趋势,我自己的编程风格已经发生了变化(例如,我不再那么棘手了,我使用了更少的go指令),但是我的风格发生了重大变化这种内循环现象。现在,我非常紧张地注视着关键的内部循环中的每个操作,试图修改我的程序和数据结构(如从示例1到示例2的更改一样),以便可以消除某些操作。这种方法的原因是:a)不需要很长时间,因为内部循环很短;b)回报是真实的;c)然后,我可以在程序的其他部分降低效率,因此它们更具可读性,更易于编写和调试。


20

我经常看到这个引号用来证明显然是错误的代码,或者那些代码(虽然其性能尚未得到衡量)可以很容易地变得更快,而又不会增加代码的大小或不影响其可读性。

总的来说,我确实认为早期的微优化可能不是一个好主意。但是,宏优化(选择O(log N)算法而不是O(N ^ 2)之类的方法)通常是值得的,应该尽早完成,因为编写O(N ^ 2)算法可能很浪费,并且然后完全抛弃它,以支持O(log N)方法。

请注意,这些单词可能是:如果O(N ^ 2)算法简单易写,那么如果结果太慢,您可以稍后将其扔掉而不会感到内。但是,如果两种算法都同样复杂,或者预期的工作量如此之大,以至于您已经知道需要使用更快的算法,那么尽早进行优化是一项明智的工程决策,从长远来看,这将减少您的总工作量。

因此,总的来说,我认为正确的方法是在开始编写代码之前找出您的选择,并有意识地为您的情况选择最佳算法。最重要的是,“过早的优化是万恶之源”这句话不是无知的借口。职业开发人员应该对一般的运营成本有一个大致的了解;他们应该知道,例如

  • 弦比数字还贵
  • 动态语言比静态类型的语言慢得多
  • 数组/向量列表相对于链表的优势,反之亦然
  • 何时使用哈希表,何时使用排序的映射以及何时使用堆
  • (如果它们与移动设备一起使用),“ double”和“ int”在台式机上具有相似的性能(FP甚至可能更快),但在没有FPU的低端移动设备上,“ double”的速度可能慢一百倍;
  • 在Internet上传输数据的速度比HDD访问速度慢,HDD的速度比RAM慢得多,RAM的速度比L1缓存和寄存器慢得多,并且互联网操作可能会无限期地阻塞(并且随时会失败)。

开发人员应该熟悉数据结构和算法的工具箱,以便他们可以轻松地使用正确的工具来完成工作。

拥有丰富的知识和个人工具箱,您几乎可以轻松进行优化。付出不必要的优化工作是很邪恶的(我承认不止一次陷入这种陷阱)。但是,当优化就像选择一个set / hashtable而不是一个数组一样简单,或者以double []而不是string []来存储数字列表时,为什么不这样做呢?我不确定,在这里我可能与Knuth不同,但我认为他是在谈论低级优化,而我在谈论高级优化。

请记住,该报价最初来自1974年。1974年,计算机速度缓慢,计算能力也很昂贵,这使一些开发人员倾向于逐行过度优化。我认为这就是Knuth所反对的。他并不是说“完全不用担心性能”,因为在1974年那将是一次疯狂的演讲。Knuth在解释如何优化。简而言之,应该只关注瓶颈,而在这样做之前,必须进行测量以找到瓶颈。

请注意,只有编写了要测量的程序,您才能找到瓶颈,这意味着必须先进行一些性能决策,然后再进行测量。如果您弄错了这些决定,有时很难更改。因此,最好对花费多少有一个大致的了解,以便在没有可用的硬数据时可以做出合理的决定。

优化的时间提早以及对性能的担忧程度取决于工作。在编写只运行几次的脚本时,根本不担心性能通常会完全浪费时间。但是,如果您为Microsoft或Oracle工作,并且正在使用一个库,其他成千上万的开发人员将以成千上万的不同方式使用该库,则可能需要对其进行优化,以便涵盖所有不同的库。用例有效。即便如此,对性能的需求也必须始终与对可读性,可维护性,美观性,可扩展性等的需求相平衡。


2
阿们 这些天来,试图证明使用错误工具进行工作的人过早地抛出过早的优化。如果您提前知道适合该工作的工具,那么没有理由不使用它。
暗恋

13

就个人而言,如上一个线程所述,我认为在您会遇到性能问题的情况下,过早的优化并不很糟糕。例如,我编写表面建模和分析软件,在那里我经常处理数千万个实体。在设计阶段对最佳性能进行计划远远优于对较弱的设计进行后期优化。

要考虑的另一件事是您的应用程序将来如何扩展。如果您认为代码寿命长,那么在设计阶段优化性能也是一个好主意。

以我的经验,后期优化可提供高昂的回报。在设计阶段通过算法选择和调整进行优化会更好。依靠探查器来了解代码的工作方式并不是获得高性能代码的好方法,您应该事先知道这一点。


这当然是正确的。我认为过早的优化是指使代码变得更加复杂/难以理解以带来不明确的好处时,这种方式仅具有局部影响(设计具有全局影响)。
Paul de Vrieze

2
这都是关于定义的。我将优化视为设计和编写以最佳方式执行的代码。一旦发现它们不够快速或高效,这里的大多数人似乎就将其视为乱搞代码。我通常在设计过程中花费大量时间进行优化。

3
首先优化设计,最后优化代码。
BCS

您的情况是正确的,但是对于大多数程序员而言,他们认为它们会遇到性能问题,但实际上他们永远不会。当处理1000个实体时,许多人担心性能,而对数据的基本测试表明,在达到1000000个实体之前,性能还不错。
Toby Allen

1
“在设计阶段进行最佳性能的计划要比对弱设计的后期优化要好得多”,“后期优化可以以高价提供微薄的回报”。对于生产的所有系统中的97%来说,可能不是正确的,但是对于许多-令人惊讶的许多-系统而言,情况并非如此。
Olof Forshell 2015年

10

实际上,我了解到,过早的非优化常常是万恶之源。

人们编写软件时,最初会遇到诸如不稳定,功能受限,易用性和性能不佳的问题。当软件成熟时,所有这些通常都会得到修复。

所有这些,除了性能。似乎没有人关心性能。原因很简单:如果软件崩溃,则将由人来修复错误,仅此而已;如果缺少某个功能,则将由人来实施并完成;如果软件性能不佳,多数情况下并不是由于缺少微优化而引起的,而是由于设计不良,没有人会去接触软件的设计。永远

看博克斯。地狱般的慢。它会变得更快吗?也许可以,但仅在百分之几的范围内。它永远不会获得与VMWare,VBox甚至QEMU等虚拟化软件相媲美的性能。因为设计上很慢!

如果软件的问题是速度很慢,则因为它非常慢,并且只能通过大量提高性能来解决。+ 10%根本不会使速度较慢的软件变快。通常,以后的优化不会使您获得超过10%的收益。

因此,如果性能对您的软件来说很重要,那么在设计软件时就应该从一开始就考虑到这一点,而不是认为“哦,是的,它很慢,但是我们可以稍后进行改进”。因为你不能!

我知道这并不完全适合您的特定情况,但是它回答了一个普遍的问题:“过早的优化真的是万恶之源吗?” -明确没有。

每个优化(如任何功能等)都必须仔细设计和实施。这包括对成本和收益的适当评估。当算法无法带来可衡量的性能提升时,请勿对其进行优化以节省一些时间。

举个例子:您可以通过内联函数来提高功能的性能,可能节省一些周期,但是与此同时,您可能会增加可执行文件的大小,增加TLB和缓存未命中的机会,这会花费数千个周期甚至分页操作,这将完全破坏性能。如果您不了解这些内容,那么“优化”可能会变得很糟糕。

愚蠢的优化比“过早的”优化更邪恶,但两者仍比过早的非优化好。


6

PO有两个问题:首先,开发时间用于不必要的工作,可用于编写更多功能或修复更多错误,其次,错误的安全意识是代码有效运行。PO通常会涉及优化代码,而这不会成为瓶颈,而不会引起注意。“过早”位表示在使用适当的测量方法确定问题之前已完成优化。

所以基本上,是的,这听起来像是过早的优化,但是除非它引入了错误,否则我不一定会放弃它-毕竟,它已经进行了优化(!)


您的意思是说“编写更多测试”而不是“编写更多功能”,对吗?:)
Greg Hewgill

1
更多功能需要更多测试:)
workmad3

嗯是的 那正是我的意思……
harriyott

2
该代码进一步引入了复杂性,因此可能不会被普遍使用。对其进行备份(以及类似操作)可使代码保持干净。
Paul de Vrieze

3

我相信这就是迈克·科恩(Mike Cohn)所说的“镀金”代码的意思,也就是说,花时间在可能不错但不必要的事情上。

他建议不要这样做。

PS“镀金”在规范方面可能是千篇一律的功能。当您查看代码时,它将采取不必要的优化,“面向未来”的类等形式。


2
我认为“镀金”与优化不同。优化通常是为了获得最大的性能,而“镀金”则是添加对产品而言并不关键但看起来/感觉很酷的“钟声”(所有额外的功能)。
Scott Dorman

3

由于理解代码没有问题,因此可以将这种情况视为例外。

但总的来说,优化会导致可读性和可读性降低,因此仅应在必要时应用。一个简单的示例-如果您只需要对几个元素进行排序,则可以使用BubbleSort。但是,如果您怀疑元素会增加而又不知道有多少,那么使用QuickSort(例如)进行优化并不是邪恶的,而是必须的。在设计程序时应考虑这一点。


1
不同意 我会说永远不要使用冒泡排序。Quicksort已成为事实上的标准,并且众所周知,并且在所有情况下都像气泡排序一样容易实现。最低的公分母不再那么低;)

1
对于项目的非常小的数字,快速排序所需的递归可以使它比一个体面的冒泡虽然慢......更何况,一个冒泡是快速排序(即quicksorting排序列表)的最坏的情况下更快
workmad3

是的,但这只是一个示例,说明如何选择满足不同需求的算法;)

是的,但是我会将quicksort作为默认排序。如果我认为Bubblesort可以提高性能,那将是一种优化,而不是相反。我选择quicksort作为我的默认设置,因为它被很好地理解并且通常更好。

2
我对默认排序的想法是库给我的(qsort()、. sort(),(sort ...),等等)。
David Thornley,

3

我发现过早优化的问题主要发生在重新编写现有代码以使其更快时。我可以看到一开始编写一些复杂的优化可能会是一个问题,但是大多数情况下,我看到过早的优化会抬起其丑陋的头,以修复没有(众所周知)的问题。

最糟糕的例子是每当我看到有人重新实现标准库中的功能时。那是一个重大的危险信号。就像,我曾经看到有人实现用于字符串操作的自定义例程,因为他担心内置命令太慢。

这导致代码更难理解(不好),并在可能没有用的工作上浪费大量时间(不好)。


3

从不同的角度来看,据我的经验,大多数程序员/开发人员都不打算成功,“原型”几乎总是成为1.0版。我对4种独立的原始产品有第一手的经验,其中优雅,性感且功能强大的前端(基本上是UI)导致了广泛的用户采用和热情。在每种产品中,性能问题都在相对较短的时间内(1-2年)开始蔓延,尤其是在更大,要求更高的客户开始采用该产品时。尽管新功能开发在管理层的优先级列表中占主导地位,但是性能很快就在问题列表中占主导地位。随着每个版本添加新功能的出现,客户越来越沮丧,这些新功能听起来不错,但由于性能问题而几乎无法访问。

因此,在“原型”中几乎没有或根本没有关注的非常基本的设计和实现缺陷成为产品(和公司)长期成功的主要绊脚石。

使用XML DOM,SQL Express和许多客户端缓存的数据,您的客户演示在您的笔记本电脑上可能看起来并表现出色。如果成功,生产系统可能会崩溃。

1976年,我们仍在讨论计算平方根或对大型数组进行排序的最佳方法,而Don Knuth的格言是针对错误的,即在设计过程的早期专注于优化这类低级例程,而不是着重解决问题。然后优化代码的本地化区域。

当有人重复这句格言作为借口,即没有编写有效的代码(C ++,VB,T-SQL或其他方式),没有正确设计数据存储或不考虑网络体系结构时,那么IMO就是在演示一种对我们工作的真实本质的浅薄理解。射线


1
哈哈,或者当三个用户的演示版成为具有一千个版本的1.0版时。
Olof Forshell 2015年

1

我想这取决于您如何定义“过早”。在编写时快速地执行低级功能并不是天生的邪恶。我认为这是对报价的误解。有时我认为报价可能会带来更多限制。不过,我会回应m_pGladiator关于可读性的评论。


1

答案是:这取决于。我将争辩说,效率对于某些类型的工作(例如复杂的数据库查询)而言至关重要。在许多其他情况下,计算机将大部分时间都花在等待用户输入上,因此优化大多数代码充其量是费力的,最不利的是适得其反。

在某些情况下,您可以针对效率或性能(感知的或真实的)进行设计-选择适当的算法或设计用户界面,以便在后台进行某些昂贵的操作。在许多情况下,进行概要分析或其他确定热点的操作将为您带来10/90的收益。

我可以描述的一个例子是我曾经为一个法院案件管理系统所做的数据模型,其中包含大约560个表格。它最初是经过规范化的(如某家大型5公司的顾问所说的是“美丽规范化”),我们只需要在其中放入四项非规范化数据:

  • 一个物化视图以支持搜索屏幕

  • 一个触发器维护的表支持另一个物化视图无法完成的搜索屏幕。

  • 一个非规范化的报告表(之所以存在,是因为当数据仓库项目得到固定时,我们必须承担一些吞吐量报告)

  • 一个用于接口的触发器维护表,该表必须搜索系统中大量不同事件中的最新事件。

这是(当时)澳大利亚最大的J2EE项目-超过100年的开发时间-并且它在数据库模式中有4个非规范化项,其中一项实际上根本不属于该项。


1

可以肯定的是,过早的优化并不是万恶之源。但是它有缺点:

  • 您在开发过程中投入了更多时间
  • 你花更多的时间测试它
  • 您会花费更多的时间来修复原本不会存在的错误

可以进行早期可见性测试,而不是进行过早的优化,以查看是否确实需要更好的优化。


1

坚持“ PMO”(即部分引号)的大多数人说,优化必须基于测量,并且直到最后才可以执行测量。

根据大型系统开发的经验,随着开发接近完成,性能测试将在最后完成。

如果我们遵循这些人的“建议”,那么所有系统的运行速度将非常缓慢。它们也很昂贵,因为它们的硬件需求比最初设想的要大得多。

我长期以来一直主张在开发过程中定期进行性能测试:它将表明新代码的存在(以前没有代码)和现有代码的状态。

  • 可以将新实现的代码的性能与现有的类似代码的性能进行比较。随着时间的推移,将为新代码的性能建立“感觉”。
  • 如果现有代码突然陷入困境,您就会了解到发生了什么情况,可以立即进行调查,而不会在以后影响整个系统的情况下进行调查。

另一个有趣的想法是在功能块级别上检测软件。系统执行时,它将收集有关功能块执行时间的信息。当执行系统升级时,可以确定哪些功能块执行的功能与早期版本中的功能相同,并且哪些功能块已退化。在软件的屏幕上,可以从帮助菜单访问性能数据。

看看是什么PMO可能会或可能不会平均佳作。

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.