25

过早优化为什么是万恶之源?

 4 years ago
source link: https://lotabout.me/2020/premature-optimization/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

因为它不是。对不关键部分的过早优化才是。Donald Knuth 这句话的原文是:

Programmers waste enormous amounts of time thinking about, or worrying about, the speed of noncritical parts of their programs, and these attempts at efficiency actually have a strong negative impact when debugging and maintenance are considered. We should forget about small efficiencies, say about 97% of the time: premature optimization is the root of all evil . Yet we should not pass up our opportunities in that critical 3%.

https://wiki.c2.com/?PrematureOptimization

翻译过来是(注意前半句话):

程序员们浪费了大量的时间去思考、担心程序中非关键部分的性能,如果考虑整体代码 的 debug 和维护的时间,则这些性能优化的开销是得不偿失的。对于约 97% 的小优化 点,我们应该忽略它们: 过早优化为什么万恶之源 。而对于剩下的关键的 3%,我 们则不能放弃优化的机会。

原文说的是:对于不关键的部分不要过早优化。而原因主要是性价比太低。

什么部分才关键?

一看预算、二看数量、三看单价。

  • “预算”是指你能容忍的时间/空间。支付宝的一笔转帐大概率需要在 200ms 内完成;一笔贷款申请 10 min 审批可能算快的;淘宝新商品也许上线 1h 后才能搜到。
  • “数量”是指某个操作被调用的次数。量变引起质变。如一般 64 位的 String 里 length 字段是 8 个字节,100 万个就被放大到 8MB 内存了。
  • “单价”就是某个操作本身的耗时了,通常需要和数量结合起来看。

换句话说,当 单价*数量 有可能大于 预算 时,要么优化数量,要么优化单价。

问题在于,我怎么知道哪部分操作可能出问题?这也许也是 Donald Knuth 最想要表达的:我们并不知道,所以要最后通过 profile 找到。借用知乎网友的回答,原文应该翻译为“盲目优化是万恶之源”。

也许上面的说法都指向“不要过早优化”,但实际编码中,多数情况下,我认为 需要 尽早优化。原因有二:

  1. 事后优化代价很大
  2. 事实上通过经验是可以预判优化点的

事后优化头很大

这里我举两个亲身经历的事件。

第一个是做规则引擎,允许用户编写规则,QA 发现在导入一个大的配置文件 (2k个规则) 需要 2 小时,在 profile 之后也很快定位到瓶颈:读写数据库的次数过多。之前的逻辑是针对界面创建规则写的,在创建规则过程中需要访问多次数据库,而批量导入时重用了单次的规则,没有合并批量的读写,导致特别慢。

思路很清晰,批量读写数据库。问题在于写入规则的逻辑很复杂,费了九牛二虎之力才理清楚逻辑改成了批量的版本,结果 bug 还是修了好几个迭代。

第二个是有一个微服务,调用的协议使用了 Avro,请求和响应中会携带数据的 Schema。经过 profile 后发现数据的序列化和反序列化占用了最多的时间,而 Schema 的内容远大于数据本身。

由于有些服务已经上线,修改协议的难度比较大,后期只能尝试增加批量的接口一定程度上缓解。

除此之外,还可能遇到“处处是热点,处处是瓶颈”的情况。比如 Rust 的编译时间很长,可是 profile 了编译器发现没有明显的热点,每个过程都占用不多,但整体性能就是优化不了。

让优化刻入骨髓

虽然提前判断出优化点很大程度上依赖于经验,但其实还是有一定“套路”的。

比如考虑“数量”,只要一个操作的次数与输入的规模相关,且输入规模可能爆炸,就要注意去优化。比如上节示例里的批量数据库读写。

比如考虑“单价”,比如上例中,Schema 内容大于数据内容,本身就是不合理的,单价的损耗太大了,就需要在设计时提前考虑。当然“单价”一般要依托于数量。

对于“处处是热点,处处是瓶颈”其实就没有特别好的方法了,只能说,要把一些知识化成习惯。例如知道了二维数组先遍历行再遍历列能更高效利用缓存,把它作为编码习惯就好了。

当然,最最重要的还是确定“预算”!接需求的时候确认输入的规模,能接受的延时等等都能让我们更好地理解优化的边界和粒度。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK