第67项:谨慎地进行优化

  有三条与优化有关的格言是每个人都应该知道的:

很多计算上的过失都被归咎于效率(没有必要达到的效率),而不是任何其他原因——甚至包括盲目地做傻事。

——William A. Wulf [Wulf72]

不要去计算效率上的一些小小的得失,在97%的情况下,不成熟的优化才是一切问题的根源。

——Donald E. Knuth [Knuth74]

在优化方面,我们应该遵守两条规则:

规则1:不要进行优化。
规则2(仅仅针对专家):还是不要进行优化——也就是说,在你还没有绝对清晰的优化方案之前,请不要进行优化。

——M. A. Jackson [Jackson75]

  所有这些格言都比Java程序设计语言的出现早了20年。它们讲述了一个关于优化的深刻真理:优化的弊大于利,特别是不成熟的优化。在优化的过程中,产生的软件可能既不快速,也不正确,而且还不容易修正。

  不要因为性能而牺牲合理的结构。要努力编写好的程序而不是快的程序 。如果好的程序不够快,它的结构将使它可以得到优化。好的程序体现了*信息隐藏(information hiding)*的原则:只要有可能,它们就会把设计决策集中在单个组件中,因此,可以改变单个决策,而不会影响到系统的其他部分(第15项)。

  这并不意味着,在完成程序之前就可以忽略性能问题。实现上的问题可以通过后期的优化而得到修正,但是,遍布全局并且限制性能的结构缺陷几乎是不可能被改正的,除非重新编写系统。在系统完成之后再改变设计的某个基本方面,会导致系统的结构很不好,从而难以维护和改进。因此,必须在设计过程中考虑到性能的问题。

  努力避免那些限制性能的设计决策 。其中最难以更改的组件是那些指定了模块之间交互关系以及模块与外界交互关系的组件。在这些设计组件之中,最主要的是API、线路层(wire-level)协议以及永久数据格式。这些设计组件不仅在事后难以甚至不可能改变,而且它们都有可能对系统本该到达的性能产生严重的限制。

  要考虑API设计决策的性能后果 。使公有的类型成为可变的(mutable),这可能会导致大量不必要的保护性拷贝(第50项)。类似地,在适合使用复合模式的共有类中使用继承,会吧这个类与它的超类永远地束缚在一起,从而人为的为限制了子类的性能(第18项)。最后一个例子,在API中使用实现类型而不是接口,会把你束缚在一个具体的实现上,即使将来出现更快的实现你也无法使用(第64项)。

  API设计对于性能的影响是非常实际的。考虑java.awt.Component类中的getSize方法。这个决定就是,这个注重性能的方法将返回Dimension实例,与此密切相关的决定是,Dimension实例是可变的,迫使这个方法的任何实现都必须为每个调用分配一个新的Dimension实例。尽管在现代的VM上分配小对象的开销并不大,但是分配数百万个不必要的对象仍然会严重地损害性能。

  在这种情况下,有几种可供选择的替换方案。理想情况下,Dimension应该是不可变的(第17项);另一种方案是,用两个方法替换getSize方法,它们分别返回Dimension对象的单个基本组件。实际上,在Java 2中,出于性能方面的原因,两个这样的方法已经被加入到了Component API中。然而,原先的客户端代码仍然可以使用getSize方法,但是仍然要承受原始API设计决策所带来的性能影响。

  幸运的是,一般而言,好的API设计也会带来好的性能。为了获得好的性能而对API进行包装,这是一种非常不好的想法 。导致你对API进行包装的性能因素可能会在平台未来的发行版本中,或者在将来的底层软件中不复存在,但是被包装的API以及由它引起的问题将永远困扰着你。

  一旦谨慎地设计了程序,并且产生了一个清晰、简明、结构良好的实现,那么就到了该考虑优化的时候了,假定你此时你对于程序的性能还不满意。

  回想一下Jackson的两条优化规则:“不要优化”以及“(仅针对专家)还是不要优化”。他可以再增加一条:在每次试图做优化之前和之后,要对性能进行测试 。你可能会惊讶于自己的发现。试图做的优化通常对于性能并没有明显的影响,有时候甚至会使性能变得更差。主要的原因在于,要猜出程序把时间花在那些地方并不容易。你认为程序慢的地方可能并没有问题,这种情况下实际上是在浪费时间去尝试优化。大多数人认为:程序把90%的时间花在了20%的代码上了。

  性能剖析工具有助于你决定应该把优化的重心放在哪里。这样的工具可以为你提供运行时的信息,比如每个方法大致上花费了多少时间、它被调用多少次。除了确定优化的重点之外,它还可以警告你是否需要改变算法。如果一个平方级(或者更差)的算法潜藏在程序中,无论怎么调整和优化都很难解决问题。你必须用更有效的算法来替换原来的算法。系统中的代码越多,使用性能剖析器就显得越发重要。这就好像要在一堆干草中寻找一根针:这堆干草越大,使用金属探测器就越有用。另一个值得特别提及的工具是jmh,它不是一个分析器,而是一个微基准测试框架,它提供了对Java代码详细性能的无与伦比的可视性[JMH]。

  相比传统语言C和C++,在Java中尝试优化的结果更需要进行测试,因为Java的性能模型较弱:各种原始操作的相对成本定义不太明确。程序员编写的内容与CPU执行的内容之间的“抽象差距(abstraction gap)”更大,这使得更可靠地预测优化的性能结果变得更加困难。有很多被神化了的性能浮出水面,结果证明是半真半假或彻头彻尾的谎言。

  Java的性能模型不仅定义不明确,而且在不同的JVM实现,或者不同的发行版本,以及不同的处理器,在它们这些当中也都各不相同。如果将要在多个JVM实现和多种硬件平台上运行程序,很重要的一点是,需要在每个Java实现上测试优化效果。有时候还必须在从不同JVM实现或者硬件平台上得到的性能结果之中进行权衡。

  自该项首次编写以来近二十年,Java软件堆栈的每个组件都变得越来越复杂,从处理器到虚拟机再到类库,以及Java运行的各种硬件都在不断增长。所有这些结合在一起,使得Java程序的性能现在比2001年更难以预测,并且相应地增加了测试它的需求【量】。

  总而言之,不要费力去编写快速的程序——应该努力编写好的程序,速度自然会随之而来。在设计系统的时候,特别是在设计API、线路层协议和永久数据格式的时候,一定要考虑性能的因素。当构建完系统之后,要测试它的性能。如果它足够快,你的任务就完成了。如果不够快,则可以在性能剖析器的帮助下,找到问题的更远,然后设法优化系统中相关的部分。第一个步骤是检查所选择的算法:再多的底层优化也无法弥补算法的选择不当。必要时重复这个过程,在每次改变之后都要测试性能,直到满意为止。

你可能感兴趣的:(Effective,Java,第三版翻译)