正如克努斯所说,
我们应该忘记效率低下的问题,例如大约97%的时间:过早的优化是万恶之源。
这是Stack Overflow经常回答诸如“哪种是最有效的循环机制”,“ SQL优化技术”之类的问题。(依此类推)。这些优化技巧问题的标准答案是分析您的代码,首先查看是否有问题,如果不是,那么就不需要您的新技术了。
我的问题是,如果特定技术有所不同,但不是特别晦涩难懂,那真的可以认为是过早的优化吗?
这是Randall Hyde的相关文章,称为《过早优化的谬误》。
Answers:
Don Knuth发起了识字编程运动,因为他认为计算机代码的最重要功能是将程序员的意图传达给人类读者。任何以性能为名使您的代码难以理解的编码实践都是过早的优化。
以优化的名义引入的某些习惯用法已经变得如此流行,以至于每个人都理解它们,并且它们已经成为期望的,而不是过早的。例子包括
在C语言中使用指针算术代替数组符号,包括使用如下惯用法
for (p = q; p < lim; p++)
在Lua中将全局变量重新绑定到局部变量,如
local table, io, string, math
= table, io, string, math
除了这些惯用语之外,捷径还招致您的危险。
所有优化都为时过早,除非
程序太慢(许多人忘记了这一部分)。
您有一个度量(概要文件或类似文件)显示优化可以改善性能。
(也可以优化内存。)
直接回答问题:
编辑:为了回应评论,使用快速排序而不是像插入排序这样的更简单算法是每个人都能理解和期望的习惯用法的另一个示例。(虽然如果你写你自己的排序例程,而不是使用库排序例程,一个希望你有一个非常好的理由。)
恕我直言,您的优化的90%应该在设计阶段根据既定的当前需求(更重要的是未来的需求)进行。如果由于应用程序无法扩展到所需的负载而不得不取出探查器,那么您将它留得太晚了,IMO将浪费大量的时间和精力,而无法纠正问题。
通常,唯一值得进行的优化是那些可以在速度方面提高一个数量级的性能,或者在存储或带宽方面提高一个倍数的优化。这些类型的优化通常与算法选择和存储策略有关,并且很难转换为现有代码。它们可能会影响您对系统语言的决策。
因此,我的建议是根据您的要求(而不是代码)及早进行优化,并考虑延长应用程序的使用寿命。
while (s[0]==' ') s = s.substring(1)
for(i=0; i<s.len && s[i]==' '; ++i); s=s.substring(i)
---,但这需要已经知道潜在的性能问题(探查器是此处不断学习的宝贵工具)。
如果尚未进行概要分析,则为时过早。
我的问题是,如果特定技术有所不同,但不是特别晦涩难懂,那真的可以认为是过早的优化吗?
嗯...所以您手头准备了两种技术,它们的成本相同(使用,阅读,修改的工作量相同),而一种效率更高。不,在这种情况下,使用效率更高的工具可能还为时过早。
中断代码编写以寻找通用编程结构/库例程的替代方案,这是偶然的机会,尽管有一个众所周知的事实,相对而言,您所编写的相对速度永远不会有任何影响,但仍有一个更高效的版本挂在某个地方。 ..这是不成熟的。
我在避免过早优化的整个概念中遇到了这个问题。
说和做之间存在脱节。
我已经进行了许多性能调整,从原本精心设计的代码中挤出了很多因素,似乎没有进行过早的优化。 这是一个例子。
在几乎每种情况下,性能欠佳的原因都是我所说的疾驰一般性,即使用抽象多层类和透彻的面向对象设计,其中简单的概念不太优雅,但完全足够。
在教授这些抽象设计概念的教材中(例如通知驱动的体系结构)和信息隐藏中,仅设置对象的布尔属性可以对活动产生无限的连锁反应,给出的原因是什么?效率。
那么,是不是过早的优化呢?
您似乎在谈论的是优化,例如使用基于散列的查找容器,而不是像索引数组那样使用索引容器,这样可以完成许多关键查找。这不是过早的优化,而是在设计阶段应决定的事情。
Knuth规则所涉及的优化类型是最小化最常见的代码路径的长度,例如通过重写汇编或简化代码来优化运行最频繁的代码,从而使其通用性降低。但是,这样做是没有用的,除非您确定代码的哪些部分需要这种优化,并且优化会(可能?)使代码更难于理解或维护,因此“过早的优化是万恶之源”。
Knuth还说,改变而不是优化总是可以改变程序使用的算法以及解决问题的方法。例如,尽管进行一些微调可能会使您的优化速度提高10%,但从根本上改变程序的工作方式可能会使它的速度提高10倍。
对此问题发表了很多其他评论:算法选择!=优化
从数据库的角度来看,在设计阶段最好不要考虑最佳设计。数据库不容易重构。一旦设计不当(无论您如何尝试隐藏过早的优化,这都是不考虑优化的设计),因为数据库对于整个系统的运作。正确地设计出适合您期望情况的最佳代码,要比等到有一百万用户和人们在整个应用程序中使用游标而大喊大叫,所花费的成本要少得多。其他优化(例如使用可编辑代码,选择看起来最好的索引等)仅在设计时才有意义。之所以称其为快速而肮脏,是有原因的。因为它永远无法正常工作,所以不要用速度来代替好的代码。坦率地说,当您了解数据库中的性能调整时,您可以编写在同一时间或更短时间内表现良好的代码,而不是编写性能不佳的代码所需要的时间。不花时间去学习什么是性能好的数据库设计,这是开发人员的惰性,而不是最佳实践。
编程时,许多参数至关重要。其中包括:
优化(追求性能)通常以牺牲其他参数为代价,并且必须与这些方面的“损失”相平衡。
当您选择性能良好的知名算法时,通常可以接受“优化”前期成本。
优化可以在从非常高级到非常低级别的不同粒度级别上进行:
从良好的架构,松散的耦合,模块化等开始。
为问题选择正确的数据结构和算法。
优化内存,尝试在缓存中容纳更多代码/数据。内存子系统比CPU慢10到100倍,并且如果将数据分页到磁盘,则它要慢1000到10,000倍。与优化单个指令相比,谨慎使用内存更有可能带来重大收益。
在每个函数中,适当使用流控制语句。(将不可变的表达式移到循环体的外部。将最常用的值放在开关/大小写中,等等)。
在每个语句中,使用最有效的表达式产生正确的结果。(乘以vs.移位等)
关于是否使用除法表达式或移位表达式的挑剔未必是过早的优化。如果您没有先优化体系结构,数据结构,算法,内存占用和流控制,那么这样做还为时过早。
当然,如果您没有定义目标绩效阈值,那么任何优化都为时过早。
在大多数情况下,可以:
A)您可以通过执行高级优化来达到目标性能阈值,因此无需摆弄表达式。
要么
B)即使执行了所有可能的优化之后,您也不会达到目标性能阈值,并且低级优化不会在性能上产生足够的差异以证明可读性下降。
以我的经验,大多数优化问题都可以在体系结构/设计或数据结构/算法级别上解决。经常(尽管并非总是)需要优化内存占用量。但是很少需要优化流控制和表达逻辑。并且在那些实际上有必要的情况下,它很少是足够的。
在极端情况下,应保留使用分析器的需求。项目的工程师应该知道性能瓶颈在哪里。
我认为“过早的优化”是非常主观的。
如果我正在编写一些代码,并且知道应该使用哈希表,那么我将这样做。我不会以某种有缺陷的方式实施它,然后等到有人遇到问题时,等一个月或一年后再提交错误报告。
与从一开始就以明显的方式优化设计相比,重新设计的成本更高。
显然,有些小事情会在第一时间被遗漏,但是这些很少是关键的设计决策。
因此:IMO本身就是代码的味道,而不是优化设计。
诺曼的答案非常好。不知何故,您通常会进行一些“过早优化”,实际上这是最佳实践,因为这样做通常是效率低下的。
例如,要添加到Norman的列表中:
for (i = 0; i < strlen(str); i++)
因为strlen在这里是一个函数调用,每次在字符串上遍历,在每个循环上调用);for (i = 0 l = str.length; i < l; i++)
并且仍然可读,所以可以。等等。但是,这种微优化绝不能以代码的可读性为代价。
值得注意的是,Knuth的原始报价来自他撰写的一篇论文,该论文提倡goto
在精心挑选和测量的区域使用,以消除热点。他的报价是一个警告,他补充说,以证明他的使用理由goto
以加速这些关键循环。
再次,如果例如n的平均值约为20,并且如果在程序中执行了大约一百万次搜索例程,则可以显着节省总体运行速度。这样的循环优化[使用
gotos
]并不难学习,正如我已经说过的那样,它们仅适用于程序的一小部分,但通常可以节省大量成本。[...]
并继续:
当今许多软件工程师的共识是要求忽略小型计算机的效率。但是我相信这仅仅是对那些精打细算的愚蠢程序员的过度反应,这些程序员无法调试或维护其“优化”程序。在既定的工程学科中,容易实现的12%的改善从未被视为微不足道;而且我认为在软件工程中应采用相同的观点。当然,我不会在单发工作上进行这样的优化,但是当要准备高质量的程序时,我不想将自己限制在拒绝我如此高效的工具上(例如,
goto
在这种情况下的陈述)。
请记住他是如何在报价中使用“优化”的(该软件实际上可能效率不高)。还请注意,他不仅批评这些“细心而笨拙的”程序员,而且还批评那些建议您应该始终忽略低效率的人。最后,到经常引用的部分:
毫无疑问,提高效率会导致滥用。程序员浪费大量时间来考虑或担心程序非关键部分的速度,而在考虑调试和维护时,这些提高效率的尝试实际上会产生严重的负面影响。我们应该忘记效率低下的情况,例如97%的时间;过早的优化是万恶之源。
...然后再介绍一下分析工具的重要性:
对程序的哪些部分真正关键进行先验判断通常是一个错误,因为使用测量工具的程序员的普遍经验是他们的直观猜测会失败。在使用此类工具七年后,我深信从现在开始编写的所有编译器都应设计为向所有程序员提供反馈,表明他们的程序哪些部分花费最大。实际上,除非已将其专门关闭,否则应自动提供此反馈。
人们到处都在滥用他的报价,常常暗示当他的整个论文都提倡微优化时,微优化还为时过早!他批评的一群人赞同这种“传统智慧”,因为他总是忽略小规模的效率,而经常误以为是他的报价,最初的报价部分是针对那些不鼓励所有形式的微观优化的人。
然而,当有经验的手拿着轮廓仪使用时,这是对适当应用微优化的支持。如今,类似的类比可能像是:“人们不应该盲目地优化软件,但是自定义内存分配器在关键区域应用以改善引用的局部性时可以发挥巨大作用,”或“使用因此,SoA rep确实很难维护,您不应该在所有地方都使用它,但是如果由经验丰富的指导者适当地应用它,它可以更快地消耗内存。 ”
每当您尝试像Knuth所提倡的那样推广精心应用的微优化时,最好放弃免责声明,以阻止新手过于兴奋和盲目地追求优化,例如重写其整个软件以供使用goto
。这部分是他在做什么。他的报价实际上是免责声明的一部分,就像有人骑摩托车跳过燃烧的火坑可能会添加免责声明,即业余爱好者不应该在家中尝试此操作,同时批评那些没有适当知识和设备而受伤的人。
他认为“过早的优化”是指那些实际上不知道自己在做什么的人所进行的优化:不知道优化是否确实需要,没有使用适当的工具进行测量,也许不了解其本质。他们的编译器或计算机体系结构,尤其是“笨拙的,笨拙的”,这意味着他们忽略了尝试捏造便士来优化(节省数百万美元)的巨大机会,而在创建代码时却无能为力更有效地调试和维护。
如果您不适合“ pennywise-and-pound-愚蠢”类别,那么即使您使用agoto
来加快关键循环的速度,也不会按照Knuth的标准过早地进行优化(这不太可能对当今的优化器有很大帮助,但是如果这样做的话,并且在真正关键的领域,您就不会过早地进行优化)。如果您实际上在真正需要的领域中应用您所做的任何工作,而他们确实从中受益,那么在Knuth的眼中,您所做的就很棒。
对我来说,过早的优化意味着在拥有一个正常工作的系统之前以及在实际剖析代码并了解瓶颈所在之前,尝试提高代码的效率。即使在那之后,在许多情况下,可读性和可维护性也应该在优化之前实现。
我认为公认的最佳实践不是过早的优化。取决于可能的性能问题(取决于使用场景)的假设时间,更多的是消耗时间。一个很好的例子:如果您花了一个星期试图优化对象的反射,然后再证明它是瓶颈,那么您就过早优化了。
除非由于用户或业务需求而发现需要从应用程序中获得更高的性能,否则没有理由担心优化。即使这样,在配置好代码之前也不要做任何事情。然后攻击耗时最多的零件。
我的看法是,如果您在不知道在不同情况下可以获得多少性能的情况下进行优化,那是过早的优化。代码的目标实际上应该使人类最容易阅读。
正如我在类似问题上发布的那样,优化规则是:
1)不要优化
2)(仅适用于专家)稍后进行优化
优化何时过早?通常。
例外可能是在您的设计中,或者是在大量使用的封装良好的代码中。过去,我研究了一些时间紧迫的代码(RSA实现),其中查看了编译器生成的汇编器并在内部循环中删除了一条不必要的指令,从而使速度提高了30%。但是,使用更复杂的算法所带来的速度却要高出几个数量级。
优化时要问自己的另一个问题是“我是否相当于在这里优化300波特调制解调器?” 。换句话说,摩尔定律会使您的优化在不久之后就变得无关紧要了。仅通过在问题上投入更多硬件就可以解决许多扩展问题。
最后但并非最不重要的一点是,在程序运行太慢之前进行优化还为时过早。如果您正在谈论的是Web应用程序,则可以在负载下运行它以查看瓶颈所在-但可能是您将遇到与大多数其他网站相同的扩展问题,并且将采用相同的解决方案。
编辑:顺便说一句,关于链接的文章,我会质疑许多假设。首先,摩尔定律在90年代停止运行并不是真的。其次,用户的时间比程序员的时间更有价值并不明显。大多数用户(至少可以说)并不是疯狂地使用每个可用的CPU周期,他们可能正在等待网络做某事。另外,当程序员的时间从实施其他事情转移到将用户在电话上时程序所做的工作减少几毫秒的时间时,这会带来机会成本。更长的时间通常不被优化,这是错误修复。