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