条款16:谨记 80-20 法则
80-20 法则说:一个程序 80% 的资源用于 20% 的代码身上。是的,80% 的执行时间花在大约 20% 的代码身上,80% 的内存被大约 20% 的代码使用,80% 的磁盘访问动作由 20% 的代码执行,80% 的维护力气花在 20% 的代码上面。这个法则在数不尽的机器平台、操作系统,以及应用程序上不断地获得验证。80-20 法则不只是一个动人的口号而已,它是系统性能议题上的一个准则,有广大的应用价值和坚实的实证基础。
当我们考虑 80-20 法则时,有一点很重要:不要过于拘泥字面上的数字。有些人喜欢更严厉的 90-10 法则,也有人喜欢同样有着实验证据而稍微宽松的数据。不论正确数字为何,基本的重点在于:软件的整体性能几乎总是由其构成要素(代码)的一小部分决定。
当程序员努力提升软件性能的时候,80-20 法则既可以简化你的生活,也可以使你的生活更复杂。从某个角度看,80-20 法则暗示,大部分时候你所产出的代码,其性能坦白说是平凡的,因为80% 的时间中,其效率不会影响系统整体性能。或许这不至于对你的自尊心造成太大打击,但应该多少会降低你的压力。从另一个角度看,这个法则暗示,如果你的软件有性能上的问题,你将面临悲惨的前景,因为你不只需要找出造成问题的那一小段瓶颈所在,还必须找出办法来大幅提升其性能。这些工作之中,最麻烦的还是找出瓶颈所在。有两个本质不同的方法可以逼近它:一种是大部分人采用的做法,另一种是正确的做法。
大部分人采用的瓶颈查找法是"猜"。用经验猜,用直觉猜,用意大利纸牌猜,或是请碟仙,或是根据谣言,或更糟的是根据代代相传流于仪式的宣称:因为网络的延迟啦,因为内存分配器没有做适当的调整啦,因为编译器没有足够的优化啦,或是因为某个愚蠢的经理拒绝我在关键的内部循环中使用 inline assembly 啦……如此的见解与判断往往夹带着一丝叫人不得不领情的嘲讽,而嘲讽者和其预判却又往往都是错误的。
大部分程序员对于程序的性能特质,都有错误的直觉,因为程序的性能特质倾向高度的非直觉性。结果,无数的努力灌注在一些绝对无法提升整体性能的程序段落上头,形成严重的精力浪费。举个例子,我们当然可以采用某些特选的算法和数据结构加入程序之中,将运算量最小化,但如果这个程序受制于 I/O(所谓 I/O-bound),那么前述种种努力对性能就一点帮助也没有。我们可以选用威力强化的 I/O 程序库(见条款23)来取代编译器所附的版本,但如果程序受制于 CPU(所谓 CPU-bound),这对性能也发挥不了什么作用。
那么,如果你面对一个迟缓的程序或是一个内存用量过大的程序,你该怎么办?80-20 法则告诉我们,如果你只是东一块西一块地改善程序,病急乱投医,头痛医头脚痛医脚,不会有太大帮助。"程序的性能特质倾向高度的非直觉性"这个事实意味,企图猜出性能瓶颈之所在,比头痛医头脚痛医脚更是不如。那么,什么是可行之道?
可行之道就是完全根据观察或实验来识别出造成你心痛的那 20% 代码。而辨识之道就是借助某个程序分析器(program profiler)。然而并不是任何分析器都足堪大任,它必须可以直接测量你所在意的资源。例如,假设你的程序太慢,你需要一个分析器告诉你程序的不同区段各花费多少时间,于是你便可以专注在特别耗时的地方加以改善,这不但可以巨幅提升局部效率,对整体效率也会有极大帮助。
那种告诉你每个语句需要多少执行时间,或是每个函数被调用多少次的分析器(profiler),其实功能有限。从性能观点来看,你不必在乎一个语句被执行多少次或一个函数被调用多少次。毕竟,程序用户或程序库客户之中,会抱怨"执行太多语句"或是"调用了太多函数"的人是很罕见的。如果你的软件够快,没有人会在乎执行太多语句;如果你的软件过慢,也没有人会因为你执行了很少的语句而原谅你。他们所在乎的只是:他们讨厌等待,如果你的程序让他们等待,他们就会讨厌你。
当然啦,知道语句被执行或函数被调用的频繁度,有时候可以让你对你的软件行为有更深刻的了解。举个例子,如果你为某个 class 产生一百个对象,而该 class 的 constructors 却被调用了数千次,那当然值得注意。此外,语句的执行次数和函数的调用次数可以间接协助你了解你无法直接测量的软件行为。如果你无法直接测量动态内存的使用,那么,知道内存分配函数和释放函数(如 new,new[] 和 delete,delete[]--见条款8)被调用的频率,至少也可以带来一些联想。
当然,即使是最好的分析器(profilers)也受制于它所使用的数据。如果你以无法重现的(unrepresentative)数据喂给你的程序,然后分析其行为,那么万一分析器引导你去调整属于 80% 那一部分代码--它们对于整体性能通常没有什么责任--你就没有立场加以抱怨。记住,分析器只能告诉你程序在某次(或某组)特定执行过程中的行为,因此如果你以无法重现的数据喂给你的程序,然后分析之,你便是在分析无法重现的行为。那可能会导致你对一些不常被使用的程序行为做优化努力,这对常用部分的整体冲击可能反而是负面的。
防范这种病态结果的最佳办法就是尽可能地以最多的数据来分析你的软件。此外,你必须确保每一组数据对于此软件的所有客户(或至少最重要的客户)而言都是可重制的(representative)。可重制数据通常不难取得,因为许多客户会很乐意让你使用他们的数据做软件分析之用。毕竟,你将因此调整你的软件以符合他们的需求,而那对你们双方都好。