ITEM 67: OPTIMIZE JUDICIOUSLY
有三个关于优化的格言是每个人都应该知道的:
与其他单一原因相比(包括盲目的愚蠢),更多的计算错误是在效率的名义下犯下的(不一定能实现)。—— William A. Wulf [Wulf72]
我们应该忘记小型的性能提升,大约97%的情况下:过早的优化是万恶之源。 — Donald E. Knuth [Knuth74]
在优化问题上,我们遵循两条规则:
规则1:不要这样做。
规则2(仅供专家使用):先不要这样做——也就是说,直到你有了一个非常清晰和完美的解决方案。
—M. A. Jackson [Jackson75]
所有这些格言都比Java编程语言早了20年。它们告诉我们关于优化的一个深刻的事实:它很容易弊大于利,特别是如果您过早地进行优化。在这个过程中,您可能会生成既不快速也不正确且不容易修复的软件。
不要为了性能而牺牲合理的架构原则。努力写出好的程序,而不是快速的程序。如果一个好的程序不够快,它的架构将允许它被优化。好的程序体现了信息隐藏的原则:在可能的情况下,它们在单个组件中本地化设计决策,因此可以在不影响系统其余部分的情况下更改单个决策(item 15)。
这并不意味着在程序完成之前可以忽略性能问题。可以通过以后的优化来修复实现问题,但是如果不重新编写系统,就不可能修复限制性能的普遍架构缺陷。事后更改设计的基本方面可能会导致难以维护和进化的结构不良的系统。因此,您必须在设计过程中考虑性能。
尽量避免限制性能的设计决策。设计中最难以更改的组件是那些指定组件之间以及与外部世界的交互的组件。这些设计组件中最主要的是 api、线级协议和持久数据格式。这些设计组件不仅在事后很难或不可能更改,而且所有这些组件都可能对系统能够达到的性能造成显著的限制。
考虑 API 设计决策的性能结果,使公共类型可变可能需要大量不必要的防御性复制(item 50)。类似地,在一个公共类中使用继承,在这个类中组合将会是合适的,它将类永远绑定到它的超类,这将人为地限制子类的性能(item 18)。最后一个示例是,在API 中使用实现类型而不是接口将您绑定到特定的实现,即使将来可能会编写更快的实现(item 64)。
API 设计对性能的影响是非常真实的。考虑 java.awt.Component 中的 getSize方法,这个性能关键的方法返回一个 Dimension 实例,Dimension 是可变的,强制这个方法的任何实现在每次调用时分配一个新的 Dimension 实例。尽管在现代 VM 上分配小对象并不昂贵,但不必要地分配数百万个对象可能会对性能造成实际损害。
存在几种 API 设计备选方案。理想情况下, Dimension 应该是不可变的(item 17);另外,getSize 也可以由两个方法替换,这两个方法返回一个 Dimension 对象的各个原始组件。实际上,出于性能原因,Java 2 中的组件中添加了两个这样的方法。但是,现有的客户端代码仍然使用 getSize 方法,并且仍然受到原始 API 设计决策的性能影响。
幸运的是,通常情况下,好的 API 设计与好的性能是一致的。为了获得良好的性能而扭曲 API 是一个非常糟糕的想法。导致您扭曲 API 的性能问题可能会在平台或其他底层软件的未来版本中消失,但是扭曲的 API 和随之而来的支持问题将永远伴随着您。
一旦您仔细地设计了您的程序并产生了一个清晰、简洁、结构良好的实现,那么就可以考虑优化了,假设您对程序的性能还不满意。
记得 Jackson 的两条优化规则是:“不要这样做” 和 “先别做(仅供专家使用)”。他本可以再加一个:在每次尝试优化之前和之后测量性能。你可能会对你的发现感到惊讶。通常,尝试的优化对性能没有可测量的影响;有时,他们让事情变得更糟。主要原因是很难猜测您的程序将时间花在哪里。程序中您认为缓慢的部分可能并没有错,在这种情况下,您将浪费时间来优化它。常识告诉我们,程序将 90% 的时间花费在 10%的代码上。
分析工具可以帮助您决定将优化工作的重点放在哪里。这些工具为您提供了运行时信息,比如每个方法大约消耗多少时间以及调用了多少次。除了关注您的调优工作之外,这还可以提醒您需要进行算法更改。如果一个二次(或更糟)算法潜伏在您的程序中,那么再多的调优也解决不了问题。你必须用一个更有效的算法来代替这个算法。系统中的代码越多,使用分析器就越重要。这就像大海捞针:干草堆越大,金属探测器就越有用。另一个值得特别提及的工具是 jmh,它不是一个分析器,而是一个微基准测试框架,为 Java 代码的详细性能提供了无与伦比的可见性。
与 C 和 C++ 等更传统的语言相比,Java 更需要度量尝试优化的效果,因为 Java 的性能模型更弱:各种基本操作的相对成本定义得更少。程序员编写的内容和 CPU 执行的内容之间的“抽象差距”更大,这使得可靠地预测优化的性能结果变得更加困难。有很多关于绩效的神话流传着,但最终被证明是半真半假或彻头彻尾的谎言。
Java的性能模型不仅定义不清,而且在不同的实现之间、不同的版本之间、不同的处理器之间也有所不同。如果您将在多个实现或多个硬件平台上运行您的程序,那么度量您的优化在每个平台上的效果是很重要的。有时您可能被迫在不同实现或硬件平台上的性能之间进行权衡。自本文首次编写以来的近 20 年里,Java 软件栈的每个组件都变得越来越复杂,从处理器到 vm 再到库,Java 所运行的硬件种类也大大增加。所有这些因素结合在一起,使得 Java 程序的性能比 2001 年更难以预测,而对其进行度量的需求也相应增加。
总而言之,不要力求写得快,而要力求写得好,效率将随之而来。但是在设计系统时,尤其是在设计 api、线级协议和持久数据格式时,一定要考虑性能。当您完成了系统的构建之后,请度量它的性能。如果足够快,你就完成了。如果没有,借助分析器找到问题的根源,并着手优化系统的相关部分。第一步是检查您对算法的选择:再多的低级优化也无法弥补糟糕的算法选择。根据需要重复这个过程,在每次更改之后度量性能,直到您满意为止。












网友评论