什么时候优化还为时过早?
Posted
技术标签:
【中文标题】什么时候优化还为时过早?【英文标题】:When is optimisation premature? 【发布时间】:2010-09-27 23:34:48 【问题描述】:正如克努斯所说,
我们应该忘记小的效率,比如大约 97% 的时间:过早优化是万恶之源。
这是 Stack Overflow 对“最有效的循环机制”、“SQL 优化技术?”等问题的回答中经常出现的内容。 (and so on)。这些优化提示问题的标准答案是分析您的代码并首先查看它是否存在问题,如果不是,则不需要您的新技术。
我的问题是,如果一种特定的技术不同但不是特别模糊或混淆,那真的可以被认为是过早的优化吗?
这是 Randall Hyde 的一篇相关文章,名为 The Fallacy of Premature Optimization。
【问题讨论】:
具有讽刺意味的是,许多高喊“过早优化是万恶之源”的人自己过早地优化了这句话:(续) “我们应该忘记小的效率,比如大约 97% 的时间:过早的优化是万恶之源。但我们不应该放弃关键的 3% 的机会”(Donald Knuth ) 我相信是 CA Hoare 说的。甚至 Knuth 也这么说。 是的,Tony Hoare 首先说“过早的优化是万恶之源”,但我相信 Knuth 引用/解释了他添加的其余部分 虽然我同意这句话是问题最常被滥用和断章取义,但根据定义,它总是正确的,因为“过早”(但它最常被错误地用作草率的设计和代码)。根据定义,如果优化发生在开发中最合适的时间点,无论是在设计期间还是任何其他时间点,都不算“为时过早”。 【参考方案1】:Don Knuth 发起了literate programming 运动,因为他认为计算机代码最重要的功能是将程序员的意图传达给人类读者。任何以性能为名使代码更难理解的编码实践都是过早的优化。
以优化的名义引入的某些习语变得如此流行,以至于每个人都理解它们并且它们已经成为预期,而不是为时过早。例子包括
在 C 中使用 指针算术而不是数组表示法,包括使用诸如
这样的习语for (p = q; p < lim; p++)
将全局变量重新绑定到局部变量在Lua中,如
local table, io, string, math
= table, io, string, math
除了这些习语,走捷径自有风险。
所有优化都为时过早,除非
程序太慢了(很多人忘记了这部分)。
您的测量结果(配置文件或类似文件)表明优化可以改善事情。
(也可以针对内存进行优化。)
直接回答问题:
如果您的“不同”技术使程序更难理解,那么这是一种过早的优化。编辑:针对 cmets,使用快速排序而不是像插入排序这样更简单的算法是每个人都理解和期望的习语的另一个例子强>。 (尽管如果您编写自己的排序例程而不是使用库排序例程,希望您有一个非常很好的理由。)
【讨论】:
根据你的定义;如果快速排序实现比冒泡排序更难阅读和理解,那么它就是过早的优化。你不能优化内存?尝试为大型稀疏矩阵查找相同的示例。恕我直言,大多数优化应该发生在设计阶段。即,很早。 @frankodwyer:但是增加指针可能比增加计数器和使用数组表示法更快,而且这将是过早的优化。 @Norman:虽然快速排序现在无处不在,但它并不是第一次发明的时候,因此 QED 是一种过早的优化,作者没有搞砸,对吧? @Software Monkey:当然。所有的 CS 研究都是在浪费纳税人的钱,应该立即停止。 任何排序算法,包括你发明的算法,如果写成单独的函数 sortQuickly(...) 和适当的 cmets,都是清晰简洁的。【参考方案2】:恕我直言,90% 的优化应该发生在设计阶段,基于感知到的当前需求,更重要的是,未来需求。如果您因为应用程序无法扩展到所需的负载而不得不使用分析器,那么您已经太晚了,IMO 将浪费大量时间和精力,而无法纠正问题。
通常,唯一值得优化的优化是在速度方面使您的性能提升一个数量级,或者在存储或带宽方面获得成倍的提升。这些类型的优化通常与算法选择和存储策略有关,并且极难逆转到现有代码中。它们可能会影响您实现系统所用语言的决定。
因此,我的建议是尽早优化,根据您的要求,而不是您的代码,并着眼于您的应用可能延长的生命周期。
【讨论】:
我不同意您的“为时已晚”的结论。当假设不成立时,基本上需要进行分析,并且需要分析器来告诉您哪些假设被破坏了。例如,我发现 Java 中 StringBuffers 的“删除位置 0 的字符”在 junit 测试中工作得很好,但对于大字符串来说却非常慢。直到分析器将其确定为罪魁祸首,我才怀疑该代码! 根据我的经验,我确实同意“当您需要分析器时,已经为时已晚” - 我的大多数性能问题不是单一的瓶颈,而是分散在多个贡献者身上。但是,我在低级代码和成本方面有很强的背景,并且本能地回避任何依赖于(显着重复)删除第一个字符串字符的东西。 +1 表示“在设计期间进行优化”。 @peterchen 只是出于好奇,你会为“删除第一个字符串字符”做什么。 @user258365:蛮力将使用不需要复制子字符串的字符串表示。对于不可变引用计数字符串来说,这“几乎是微不足道的”。或者,算法更改,例如替换(伪代码)while (s[0]==' ') s = s.substring(1)
for(i=0; i<s.len && s[i]==' '; ++i); s=s.substring(i)
--- 但这需要已经知道潜在的性能问题(分析器是在这里不断学习的宝贵工具)。
@ThorbjørnRavnAndersen,我曾担任顾问,帮助团队完成一个项目,但由于没有计划严重的性能问题(除了意大利面条代码),这是不可能的。它应该显示所有患者病史的时间顺序图。对整个数据发出了一个请求,就像谷歌地图获取整个世界一样。开发糟糕的代码,期望稍后进行分析导致项目失败。【参考方案3】:
如果您还没有进行剖析,那还为时过早。
【讨论】:
我同意它背后的想法,但也:除非实现完全受 CPU 周期的约束,否则很难获得既可重现又可推广的测量 - 而且越稳定,它也不太现实。 我对上述答案的问题是,它意味着您无法在编码之前优化算法。我的工作方式倾向于设计算法以满足功能需求。查看它是否可能无法满足性能要求(例如,高复杂度并可能影响大型数据集)并在开始编码之前优化算法。优化只是为了达到最佳解决方案而进行的细化,通常在设计阶段最有效地完成。 我不同意。 Knuth 说的是小效率。优化通常发生在设计阶段。它涉及选择适当的数据结构和算法,这些数据结构和算法通常会对性能产生很大影响,并且以后不一定可以交换。 @haslersn :“Knuth 谈论的是小效率” Donald Knuth:“当今许多软件工程师所共有的传统智慧要求忽略小效率;但我认为这只是对小效率的过度反应。滥用(...)在已建立的工程学科中,12% 的改进很容易获得,但绝不会被认为是微不足道的(...)”【参考方案4】:我的问题是,如果一个特定的 技术不同但不一样 特别模糊或混淆, 这真的可以被认为是 过早优化?
嗯...所以你手头有两种技术,成本相同(使用、阅读、修改的努力相同),一种更有效。不,在这种情况下,使用更有效的方法不会为时过早。
中断您的代码编写以寻找常见编程结构/库例程的替代方案,即使您知道您正在编写的内容的相对速度永远不会有更高效的版本挂在某个地方,实际上很重要... 那为时过早。
【讨论】:
同意,如果您知道一种算法对您的用例更有效,请务必使用更有效的一种。如果您不知道最有效的算法,请使用您所拥有的并稍后进行分析以查看是否存在问题。【参考方案5】:这是我在避免过早优化的整个概念中看到的问题。
说和做之间存在脱节。
我已经进行了大量的性能调整,从原本设计良好的代码中挤出大量因素,似乎没有过早优化。 Here's an example.
几乎在所有情况下,性能欠佳的原因都是我所说的泛滥的普遍性,即使用抽象的多层类和彻底的面向对象设计,其中简单的概念将是不那么优雅,但完全足够。
在讲授这些抽象设计概念的教材中,例如通知驱动的架构和信息隐藏,简单地设置对象的布尔属性可以产生无限的活动连锁反应,给出的原因是什么? 效率。
那么,这是不是过早的优化?
【讨论】:
我喜欢这个答案,因为它说明了抽象和泛化的主要问题之一。当您概括类层次结构以支持更广泛的用例时,很容易严重损害最典型用例的性能。也很容易锁定一个提供给定功能的类,而无需检查该功能是否以预期使用规模可接受的性能水平提供。 "其中简单的概念会不那么优雅但完全足够" 当简单的代码满足要求时,复杂的代码很少比简单的代码更优雅。 (虽然,我认为如果有人试图在更复杂的情况下执行它,你必须确保你的简单代码实际上会爆炸并清楚地表明不受支持的状态/输入。)【参考方案6】:首先,让代码正常工作。其次,验证代码是否正确。第三,要快。
在第 3 阶段之前完成的任何代码更改 绝对为时过早。我不完全确定如何对之前做出的设计选择进行分类(比如使用非常适合的数据结构),但我更倾向于使用易于编程的抽象,而不是那些性能良好的抽象,直到我在一个阶段,我可以开始使用分析并拥有一个正确的(虽然通常很慢)参考实现来比较结果。
【讨论】:
【参考方案7】:从数据库的角度来看,在设计阶段不考虑优化设计充其量是鲁莽的。数据库不容易重构。一旦它们设计不佳(这是一个不考虑优化的设计,无论您如何试图隐藏过早优化的废话),几乎永远无法从中恢复,因为数据库对整个系统的运行。考虑到您所期望的情况的最佳代码,正确设计的成本要低得多,而不是等到有一百万用户并且人们因为您在整个应用程序中使用游标而尖叫。其他优化,例如使用 sargeable 代码、选择看起来最好的索引等,只有在设计时才有意义。快速和肮脏被称为是有原因的。因为它永远不能很好地工作,所以不要用快速来代替好的代码。同样坦率地说,当您了解数据库中的性能调优时,您可以编写更可能在同一时间或更少时间内执行良好的代码,而不是编写性能不佳的代码。不花时间了解什么是性能良好的数据库设计是开发人员的懒惰,而不是最佳实践。
【讨论】:
【参考方案8】:您似乎在谈论的是优化,例如使用基于哈希的查找容器与索引容器(如数组),当需要完成大量关键查找时。这不是过早的优化,而是您应该在设计阶段决定的事情。
Knuth 规则所涉及的优化类型是最小化最常见代码路径的长度,通过例如重写汇编或简化代码来优化运行最多的代码,使其不那么通用。但是,除非您确定代码的哪些部分需要这种优化,否则这样做是没有用的,并且优化会(可能?)使代码更难理解或维护,因此“过早的优化是万恶之源”。
Knuth 还说,最好不要优化,而是更改程序使用的算法,即解决问题的方法。例如,虽然稍作调整可能会使您的优化速度提高 10%,但从根本上改变程序的工作方式可能会使其速度提高 10 倍。
对很多其他 cmet 在这个问题上发表的回应:算法选择!=优化
【讨论】:
【参考方案9】:格言的重点是,通常,优化是错综复杂的。而且通常,架构师/设计师/程序员/维护人员需要清晰简洁的代码才能了解正在发生的事情。
如果特定优化清晰简洁,请随意尝试(但请返回并检查该优化是否有效)。关键是在整个开发过程中保持代码简洁明了,直到性能收益超过编写和维护优化的成本。
【讨论】:
实际上,相当多的“优化”归结为为工作选择合适的算法;这是一项具有高水平结果的高水平活动——与 Knuth 名言中的“小效率”相去甚远。【参考方案10】:我尝试仅在确认性能问题时进行优化。
我对过早优化的定义是“将精力浪费在未知的性能问题代码上”。绝对有优化的时间和地点。但是,诀窍是仅在对应用程序性能重要且额外成本超过对性能造成的影响时才花费额外成本。
在编写代码(或数据库查询)时,我努力编写“高效”代码(即,以合理的最简单逻辑快速且完全执行其预期功能的代码。)请注意,“高效”代码不一定与“优化”代码。优化通常会在代码中引入额外的复杂性,从而增加该代码的开发和维护成本。
我的建议:尽量只在可以量化收益时支付优化成本。
【讨论】:
【参考方案11】:在编程时,许多参数至关重要。其中包括:
可读性 可维护性 复杂性 稳健性 正确性 性能 开发时间优化(追求性能)通常以牺牲其他参数为代价,并且必须平衡这些领域的“损失”。
当您可以选择性能良好的知名算法时,预先“优化”的成本通常是可以接受的。
【讨论】:
您缺少上面列出的最重要的 QA 参数;满足要求。如果一个软件不能满足目标受众的要求,那么所有其他参数都是没有意义的。如果性能不可接受,则未满足要求。 这可以说是被正确性覆盖了。此外,“尽可能快”意义上的“性能”在需求中很少见,即使是 Ola 的观点,它是与其他需求的权衡仍然是正确的。【参考方案12】:优化可以发生在不同的粒度级别,从非常高级别到非常低级别:
从良好的架构、松耦合、模块化等开始。
为问题选择正确的数据结构和算法。
优化内存,尝试在缓存中容纳更多代码/数据。内存子系统比 CPU 慢 10 到 100 倍,如果您的数据被分页到磁盘,它会慢 1000 到 10,000 倍。与优化单个指令相比,对内存消耗保持谨慎更有可能带来重大收益。
在每个函数中,适当使用流控制语句。 (将不可变表达式移到循环体之外。将最常见的值放在开关/case 等的首位)
在每个语句中,使用产生正确结果的最有效的表达式。 (乘法与移位等)
挑剔是使用除法表达式还是移位表达式并不是必然过早的优化。如果您不首先优化架构、数据结构、算法、内存占用和流控制就这样做还为时过早。
当然,如果您不定义目标性能阈值,任何优化都为时过早。
在大多数情况下,要么:
A)您可以通过执行高级优化来达到目标性能阈值,因此不必摆弄表达式。
或
B) 即使在执行了所有可能的优化之后,您也不会达到目标性能阈值,并且低级优化在性能上的差异不足以证明可读性损失是合理的。
根据我的经验,大多数优化问题都可以在架构/设计或数据结构/算法级别解决。经常(尽管不总是)需要优化内存占用。但是很少需要优化流控制和表达逻辑。在实际需要的情况下,它很少是足够的。
【讨论】:
【参考方案13】:诺曼的回答非常好。不知何故,您通常会做一些“过早的优化”,这实际上是最佳实践,因为众所周知,这样做是完全没有效率的。
例如,添加到诺曼的列表中:
在 Java(或 C# 等)中使用 StringBuilder 连接而不是 String + String(在循环中); 避免在 C 中循环,如:for (i = 0; i < strlen(str); i++)
(因为这里的 strlen 是一个函数调用,每次遍历字符串,在每个循环中调用);
似乎在大多数 javascript 实现中,这样做也更快for (i = 0 l = str.length; i < l; i++)
,而且它仍然可读,所以没关系。
等等。但这种微优化绝不应该以牺牲代码的可读性为代价。
【讨论】:
【参考方案14】:在极端情况下应保留使用分析器的需要。项目的工程师应该知道性能瓶颈在哪里。
我认为“过早的优化”是非常主观的。
如果我正在编写一些代码并且我知道我应该使用哈希表,那么我会这样做。我不会以某种有缺陷的方式实现它,然后等待一个月或一年后有人遇到问题时收到错误报告。
重新设计比从一开始就以明显的方式优化设计成本更高。
很明显,第一次会错过一些小事情,但这些很少是关键的设计决策。
因此:不优化设计本身就是 IMO 的代码异味。
【讨论】:
问题是瓶颈经常出现在你从未想过会成为问题的代码部分。剖析无需伪装,而是显示了项目的实际成本中心。最好从一开始就做显而易见的事情,但对于其他所有事情,都有剖析。【参考方案15】:值得注意的是,Knuth 的原始引用来自他撰写的一篇论文,该论文宣传在精心挑选和测量的区域中使用 goto
作为消除热点的一种方式。他的引用是他添加的一个警告,以证明他使用goto
来加速这些关键循环的理由。
[...] 再次,这明显节省了整体运行速度, 例如,如果 n 的平均值约为 20,并且如果搜索例程 在程序中执行大约一百万次左右。这样的循环 优化 [使用
gotos
] 并不难学,而且正如我所学 说,它们只适用于程序的一小部分,但它们 往往会带来可观的节省。 [...]
然后继续:
当今许多软件工程师所共有的传统智慧 呼吁忽略小事的效率;但我相信这是 只是对他们看到的滥用行为的过度反应 不会调试或维护的傻瓜程序员 他们的“优化”程序。在已建立的工程学科中 12% 的改进,很容易获得,从不被认为是微不足道的;和我 相信同样的观点应该在软件工程中占上风。的 当然我不会费心在一次性工作上进行这样的优化, 但是当它是一个准备质量程序的问题时,我不想要 限制自己使用无法获得这种效率的工具 [即,
goto
在这种情况下的陈述]。
请记住他如何在引号中使用“优化”(该软件可能实际上并不高效)。还要注意他不仅批评了这些“一分钱一分货”的程序员,还批评了那些建议你总是忽略小的低效率的人。最后,到经常被引用的部分:
毫无疑问,效率的圣杯会导致滥用。 程序员浪费大量时间思考或担忧 关于,他们程序的非关键部分的速度,以及这些 提高效率的尝试实际上会产生强烈的负面影响 考虑调试和维护。我们应该忘记小 效率,比如说 97% 的时间;过早优化是根本 万恶之源。
...然后更多地了解分析工具的重要性:
先验地判断一个项目的哪些部分通常是错误的 程序真的很关键,因为普遍的经验 一直在使用测量工具的程序员 直觉猜测失败。在使用这些工具七年之后, 我已经确信从现在开始编写的所有编译器都应该是 旨在为所有程序员提供反馈,指出什么 他们的部分项目成本最高;确实,这个反馈 应自动提供,除非已明确 关闭。
人们到处都在滥用他的话,当他的整篇论文都在提倡微优化时,他经常暗示微优化还为时过早!他批评的一群人呼应了这种“传统智慧”,因为他总是忽略小事的效率,他们经常滥用他的引述,该引述最初是针对那些不鼓励所有形式的微优化的类型的人。 .
然而,当经验丰富的手持分析器使用时,它是支持适当应用微优化的引用。今天的类比等价物可能是,“人们不应该盲目地优化他们的软件,但自定义内存分配器在应用于关键领域以提高引用的局部性时可以产生巨大的影响,” 或, "使用 SoA 代表的手写 SIMD 代码确实很难维护,您不应该到处使用它,但如果由经验丰富且有指导的人适当应用,它会更快地消耗内存。"
每当您尝试推广如 Knuth 上面所推广的仔细应用的微优化时,最好加入免责声明,以阻止新手过于兴奋和盲目地尝试优化,例如重写他们的整个软件以使用goto
。这部分是他正在做的。他的引述实际上是一个大免责声明的一部分,就像骑摩托车跳过燃烧的火坑的人可能会添加一个免责声明,即业余爱好者不应该在家里尝试这个,同时批评那些在没有适当知识和设备的情况下尝试并受伤的人.
他认为“过早的优化”是那些实际上不知道自己在做什么的人应用的优化:不知道优化是否真的需要,没有使用适当的工具进行衡量,也许不明白他们的编译器或计算机体系结构的性质,最重要的是,是“一分钱一磅的愚蠢”,这意味着他们忽略了优化(节省数百万美元)的大机会,试图捏几分钱,同时创建代码他们无法再有效地调试和维护。
如果您不属于“一分钱一磅-愚蠢”类别,那么即使您使用goto
来加快关键进程,您也不会过早按照 Knuth 的标准进行优化循环(这对当今的优化器不太可能有太大帮助,但如果确实如此,并且在真正关键的领域,那么您就不会过早地进行优化)。如果您实际上将您正在做的任何事情应用到真正需要的领域并且他们真正从中受益,那么您在 Knuth 眼中就做得很好。
【讨论】:
【参考方案16】:对我来说,过早的优化意味着在你拥有一个工作系统之前尝试提高你的代码效率,在你真正分析它并知道瓶颈在哪里之前。即使在那之后,在许多情况下,可读性和可维护性也应该先于优化。
【讨论】:
【参考方案17】:我不认为公认的最佳做法是过早的优化。更多的是关于根据使用场景在潜在性能问题的假设上花费时间。一个很好的例子:如果你花了一周的时间试图优化一个对象的反射,然后你没有证明它是一个瓶颈,那么你就过早地优化了。
【讨论】:
【参考方案18】:除非您发现由于用户或业务需要而需要提高应用程序的性能,否则几乎没有理由担心优化。即便如此,在你分析你的代码之前不要做任何事情。然后攻击最耗时的部分。
【讨论】:
【参考方案19】:我的看法是,如果您在不知道在不同情况下可以获得多少性能的情况下优化某些东西,那么这就是过早的优化。代码的目标应该是让人类更容易阅读。
【讨论】:
【参考方案20】:正如我在类似问题上发布的那样,优化规则是:
1) 不要优化
2)(仅限专家)稍后优化
什么时候优化过早?通常。
异常可能在您的设计中,或者在大量使用的封装良好的代码中。在过去,我研究过一些时间关键代码(一个 RSA 实现),其中查看编译器生成的汇编器并在内部循环中删除一条不必要的指令可以提高 30% 的速度。但是,使用更复杂的算法所带来的加速比这要高出几个数量级。
在优化时要问自己的另一个问题是“我是否在做相当于优化 300 波特调制解调器的工作?”。换句话说,摩尔定律是否会在不久之后使您的优化变得无关紧要。只需在问题上投入更多硬件即可解决许多扩展问题。
最后但并非最不重要的是,在程序运行太慢之前进行优化还为时过早。如果您说的是 Web 应用程序,您可以在负载下运行它以查看瓶颈在哪里 - 但您可能会遇到与大多数其他网站相同的扩展问题,并且适用相同的解决方案。
编辑:顺便说一下,关于链接的文章,我会质疑所做的许多假设。首先,摩尔定律在 90 年代停止工作是不正确的。其次,用户的时间比程序员的时间更有价值这一点并不明显。大多数用户(至少可以说)无论如何都不会疯狂地使用每个可用的 CPU 周期,他们可能正在等待网络做某事。另外,当程序员的时间从实现其他事情上转移到用户打电话时程序执行的事情上减少几毫秒时,还有机会成本。任何比这更长的时间通常都不是优化,而是错误修复。
【讨论】:
以上是关于什么时候优化还为时过早?的主要内容,如果未能解决你的问题,请参考以下文章
软件工程网络15个人阅读作业2 201521123056 吴剑通