什么时候优化过早?
正如Knuth所说,
我们应该忘记小效率,大约97%的时间:不成熟的优化是万恶之源。
这是Stack Overflow常常遇到的问题,比如“哪个是最有效的循环机制”,“SQL优化技术? ( 等等 )。 这些优化技巧的标准答案是分析你的代码,看看它是否是一个问题,如果不是,那么你的新技术是不需要的。
我的问题是,如果一个特定的技术是不同的,但不是特别模糊或混淆,可真的被认为是一个不成熟的优化?
以下是Randall Hyde撰写的一篇名为“过早优化的谬误”的文章。
Don Knuth开始了文字编程运动,因为他相信计算机代码最重要的function是将程序员的意图传达给读者 。 任何使您的代码难以理解的性能名称的编码实践是一个过早的优化。
以优化名义引进的某些成语已经变得如此受欢迎,以至于每个人都了解它们,而且它们已经成为预期 ,并不是不成熟的。 例子包括
-
在C中使用指针算术而不是数组符号 ,包括使用这样的习语
for (p = q; p < lim; p++)
-
将全局variables重新绑定到 Lua中的局部variables ,如
local table, io, string, math = table, io, string, math
除了这样的习语之外,还可以快捷地处理你的危险 。
除非,所有优化都是不成熟的
-
一个程序太慢了 (很多人都忘记了这个部分)。
-
你有一个测量 (简介或类似),显示优化可以改善的事情 。
(也可以对内存进行优化。)
直接回答问题:
- 如果你的“不同”技术使程序难以理解 ,那么这是一个不成熟的优化 。
编辑 :在回应评论, 使用快速sorting,而不是一个更简单的algorithm,如插入sorting是另一个每个人都理解和期望的成语的例子。 (虽然如果你编写自己的sorting例程,而不是使用库sorting例程,那么希望你有一个很好的理由。)
恕我直言,90%的优化应该发生在devise阶段,基于percieved目前,更重要的是,未来的要求。 如果因为应用程序没有按照所需的负载进行扩展,你必须拿出一个分析器,那么你就太晚了,而IMO将会浪费大量的时间和精力,而不能纠正这个问题。
通常情况下,唯一有价值的优化是那些在速度方面获得数量级性能提升的存储或带宽乘法器。 这些types的优化通常与algorithmselect和存储策略相关,并且极难反转成现有的代码。 它们可能会影响你实现系统的语言的决定。
所以,我的build议,根据您的要求,尽早优化,而不是您的代码,并期待您的应用程序可能延长寿命。
如果你还没有分析,那还为时过早。
我的问题是,如果一个特定的技术是不同的,但不是特别模糊或混淆,可真的被认为是一个不成熟的优化?
嗯…所以你有两个技巧准备在手,成本相同(同样的努力,使用,阅读,修改),一个是更有效的。 不,在这种情况下,使用效率更高的人是不成熟的。
打断你的代码写作,寻找常见的编程结构/库例程的替代品,尽pipe对于所有你知道的,你所知道的相对速度永远不会有影响。 那还为时过早。
首先,让代码工作。 其次,validation代码是否正确。 第三,快速。
在阶段#3之前完成的任何代码更改肯定是不成熟的。 我不完全确定如何对之前做出的deviseselect进行分类(比如使用非常合适的数据结构),但我更愿意转向使用易于编程的抽象,而不是那些performance良好的抽象,直到我在一个阶段,我可以开始使用分析,并有一个正确的(虽然经常慢)参考实现来比较结果。
这是我看到的避免过早优化的整个概念的问题。
说和做这件事之间有一个脱节。
我已经做了大量的性能调整,从其他devise良好的代码中榨取了大量的因素,似乎没有过早的优化。 这是一个例子。
在几乎所有情况下,性能欠佳的原因就是我所说的“ 舞动泛化” ,就是抽象的多层类和彻底的面向对象devise的使用,简单的概念将不够优雅但完全足够。
而在教这些抽象的devise概念的教材中,比如通知驱动的架构,以及简单地设置对象的布尔属性的信息隐藏都会带来无限的活动涟漪效应,是什么原因呢? 效率 。
那么,是不是过早的优化?
你似乎在谈论的是像使用一个基于散列的查找容器与索引的容器之类的优化,如许多密钥查找将完成。 这不是不成熟的优化,但是在devise阶段你应该做出决定。
Knuth规则的优化types是最小化最常见代码path的长度,优化运行最多的代码,例如在汇编中重写或简化代码,使其不那么一般。 但是直到你确定哪部分代码需要这种优化和优化(可以使代码更难理解或维护),这样做是没有用的,因此“过早优化是万恶之源”。
Knuth还表示,最好不要改变程序使用的algorithm,而是改变问题的方法。 例如,虽然稍微调整可能会使优化速度提高10%,但从根本上改变程序的工作方式可能会使速度提高10倍。
针对这个问题上发表的很多其他评论的反应:algorithmselect!=优化
从数据库的angular度来看,在devise阶段不考虑优化devise充其量是蛮荒的。 数据库不容易重构。 一旦他们devise得不好(这是一个不考虑优化的devise,不pipe你如何试图隐藏在过早优化的废话之后),几乎永远不能从数据库太基本的整个系统的运行。 考虑到预期情况的最佳代码,要比等到有一百万用户和人们尖叫,因为你在整个应用程序中使用游标,devise正确的代价要低得多。 其他的优化,如使用可重用的代码,select什么看起来是最好的索引等,只有在devise时才有意义。 这就是为什么快速和肮脏被称为的原因。 因为它不能很好地工作,所以不要使用速度代替好的代码。 另外坦率地说,当你理解数据库中的性能调优时,你可以编写更可能在同一时间执行得更好的代码,或者编写代码执行不佳的代码。 没有花时间学习什么是好的performance,数据库devise是开发懒惰,而不是最佳实践。
格言的要点是, 通常情况下 ,优化是复杂的和复杂的。 通常 ,build筑师/devise师/程序员/维护人员需要清晰简洁的代码才能理解正在发生的事情。
如果一个特定的优化是清晰和简明的,请随时尝试一下(但要回去检查一下这个优化是否有效)。 重点是在整个开发过程中保持代码的清晰和简洁,直到性能的好处超过编写和维护优化的诱导成本。
我试图只在性能问题得到确认时才进行优化。
我对过早优化的定义是“努力浪费在代码上,而这些代码并不是一个性能问题”。 有绝对的时间和地点进行优化。 然而,诀窍是花费额外的成本,只在重要的应用程序的性能和额外的成本超过性能的打击。
在编写代码(或数据库查询)时,我努力写出“高效”的代码(也就是执行其预期function的代码,用最简单的逻辑来快速完整地完成)。注意,“高效”代码不一定与“优化”码。 优化通常会在代码中增加复杂性,从而增加代码的开发和维护成本。
我的build议是:只有在您可以量化收益时,才支付最优化的成本。
编程时,一些参数是至关重要的。 其中包括:
- 可读性
- 可维护性
- 复杂
- 稳健性
- 正确性
- 性能
- 开发时间
优化(去performance)往往是以牺牲其他参数为代价的,必须与这些领域的“损失”相平衡。
如果您可以select性能良好的众所周知的algorithm,则“优化”前期成本通常是可以接受的。
优化可以发生在不同的粒度级别,从非常高级到非常低的级别:
-
从一个好的架构,松耦合,模块化等开始
-
为这个问题select正确的数据结构和algorithm。
-
优化内存,试图使更多的代码/数据在caching中。 内存子系统比CPU慢10到100倍,如果你的数据被分页到磁盘,速度会慢1000到10000倍。 对内存消耗持谨慎态度,比优化个别指令更有可能带来重大收益。
-
在每个function中,适当使用stream量控制语句。 (把不可变的expression式移到循环体的外面,把最常见的值放在开关/例子中,等等)
-
在每个语句中,使用产生正确结果的最有效的expression式。 (乘与换等)
挑选是否使用分解expression式或移位expression式不一定是不成熟的优化。 如果没有首先优化体系结构,数据结构,algorithm,内存占用空间和stream量控制,那还为时过早。
当然,如果不定义目标性能阈值, 任何优化都是不成熟的。
在大多数情况下,
A)您可以通过执行高级优化来达到目标性能阈值,所以不必操纵expression式。
要么
B)即使在执行了所有可能的优化之后,您也不会达到目标性能阈值,而低级优化在性能上没有足够的差异来certificate丢失可读性。
根据我的经验,大多数优化问题可以在架构/devise或数据结构/algorithm级别解决。 通常(虽然不总是)需要优化内存占用。 但是很less需要优化stream量控制和expression式逻辑。 而在那些实际需要的情况下,这还不够。
诺曼的回答非常好。 不知何故,你经常做一些“不成熟的优化”,这实际上是最佳实践,因为否则就被认为是完全没有效率的。
例如,添加到诺曼的名单:
- 在Java(或C#等)中使用StringBuilder连接而不是String + String(在循环中);
- 避免循环使用C:
for (i = 0; i < strlen(str); i++)
(因为strlen是一个函数调用,每次在每个循环中调用)。 - 看来在大多数JavaScript实现中,
for (i = 0 l = str.length; i < l; i++)
也是更快的,它仍然是可读的,所以可以。
等等。 但是这样的微观优化不应该以牺牲代码的可读性为代价。
在极端情况下应该使用探查器。 项目的工程师应该知道性能瓶颈在哪里。
我认为“不成熟的优化”是非常主观的。
如果我正在编写一些代码,我知道我应该使用一个Hashtable,那么我会这样做。 我不会以某种有缺陷的方式来实施它,然后等待错误报告在一个月或一年之后到达,当有人遇到问题时。
重新devise比从一开始就以明显的方式优化devise成本更高。
显然有些小事情会在第一时间被错过,但这些很less是关键的devise决策。
因此:不优化devise是IMO本身的代码味道。
我并不认为公认的最佳做法是过早的优化。 更重要的是根据使用情况,在可能出现性能问题的情况下如何燃烧时间。 一个很好的例子:如果你在一个星期内试图优化reflection一个对象,然后certificate它是一个瓶颈,那么你就过早地进行了优化。
对我来说,过早优化意味着在你有一个工作系统之前,在实际分析它并知道瓶颈的位置之前,试图提高你的代码的效率。 即使在此之后,在许多情况下,可读性和可维护性应该优化。
我看到的方式是,如果在不知道在不同情况下获得多less性能的情况下进行优化,那么这是一个不成熟的优化。 代码的目标应该是真正让人们阅读起来最简单。
除非您发现您的应用程序需要更多性能,否则无论是用户还是业务需求,都没有理由担心优化。 即使如此,除非您已经对代码进行了剖析,否则不要做任何事情。 然后攻击花费最多时间的部件。
正如我在类似的问题上发布,优化的规则是:
1)不要优化
2)(仅限专家)稍后优化
什么时候优化过早? 通常。
这个例外可能是在你的devise中,或者是被大量使用的封装良好的代码中。 在过去,我一直在研究一些时间关键的代码(一个RSA实现),在那里查看编译器生成的汇编程序,并在内部循环中删除单个不必要的指令,从而实现了30%的加速。 但是,使用更复杂的algorithm的速度比这个数量级要高出数倍。
在优化时问自己的另一个问题是“我在做相当于在这里优化300波特调制解调器吗? 。 换句话说,摩尔定律在不久之前会使你的优化无关紧要。 很多扩展问题都可以通过在问题上投入更多的硬件来解决。
最后但并非最不重要的是,在程序进行得太慢之前进行优化还为时过早。 如果您正在讨论的是Web应用程序,那么您可以在负载下运行它以查看瓶颈的位置 – 但是可能会出现与其他大多数站点一样的扩展问题,并且也会应用相同的解决scheme。
编辑:顺便说一下,关于链接的文章,我会质疑许多假设。 首先,摩尔定律在九十年代停止工作是不正确的。 其次,用户的时间比程序员的时间更有价值并不明显。 大多数用户(至less可以说)不是疯狂地使用每个可用的CPU周期,他们可能正在等待networking做些什么。 另外,如果程序员的时间从实现其他方面转移到在用户打开电话时程序所做的几毫秒内,则会有一定的机会成本。 比这个更长的东西通常不是最优化的,它是修正错误的。