有三条关于优化的格言是每个人都应该知道的:
比起其他任何单一的原因(包括盲目的愚蠢),更多的计算罪恶是在效率的名义下犯下的(不一定能实现)。
——William A. Wulf [Wulf72]
我们应该忘记小的效率,比如97%的时间:过早的优化是万恶之源。
——Donald E. Knuth [Knuth74]
在优化问题上,我们遵循两条规则:
规则1。不要这样做。
规则2(只适用于专家)。先不要这样做——也就是说,直到你有了一个完全清晰的、未优化的解决方案。
——M. A. Jackson [Jackson75]
所有这些格言都比Java编程语言早了20年。它们告诉我们一个关于优化的深刻事实:这很容易弊大于利,尤其是如果您过早地进行优化。此过程中,您可能会生成既不快速也不正确且无法轻松修复的软件。
不要为了性能而牺牲合理的架构原则。努力编写好的程序,而不是快速的程序。如果一个好的程序不够快,它的架构将允许对其进行优化。好的程序体现了信息隐藏的原则:在可能的情况下,它们在单个组件中本地化设计决策,因此可以在不影响系统其余部分的情况下更改单个决策(item15)。
这并不意味着在程序完成之前可以忽略性能问题。实现问题可以通过以后的优化来解决,但是如果不重写系统,就不可能解决限制性能的普遍架构缺陷。事后更改设计的基本方面可能导致结构不良的系统难以维护和进化。因此,您必须在设计过程中考虑性能。
尽量避免限制性能的设计决策。设计中最难以更改的组件是那些指定组件之间以及与外部世界的交互的组件。这些设计组件中最主要的是api、线级协议和持久数据格式。这些设计组件不仅难以或不可能在事后更改,而且所有这些组件都可能对系统能够达到的性能造成重大限制。
考虑API设计决策的性能结果。使公共类型可变可能需要大量不必要的防御性复制(item50 )。类似地,在一个公共类中使用继承(在这个类中组合将是合适的)将该类永远绑定到它的超类,这会人为地限制子类的性能( item18)。最后一个例子,在API中使用实现类型而不是接口将您绑定到特定的实现,即使将来可以编写更快的实现(item64)。
API设计对性能的影响是非常真实的。考虑java.awt.Component类中的getSize方法。这个性能关键的方法返回一个维度实例的决定,加上维度实例是可变的决定,强制该方法的任何实现在每次调用时分配一个新的维度实例。尽管在现代VM上分配小对象并不昂贵,但不必要地分配数百万个对象会对性能造成实际损害。
存在几种API设计替代方案。理想地,Dimension 应该是不可变的( item17)。或者,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、线级协议和持久数据格式时。当您完成了系统的构建之后,请度量它的性能。果足够快,就完成了。如果没有,利用分析器找到问题的根源,并对系统的相关部分进行优化。第一步是检查算法的选择:再多的底层优化也不能弥补算法选择的不足。根据需要重复这个过程,在每次更改之后测量性能,直到您满意为止。
本文写于2019.7.19,历时1天
网友评论