Effective-Java 第三版中文版 明智审慎地进行优化

67. 明智审慎地进行优化

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

比起其他任何单一的原因(包括盲目的愚蠢),很多计算上的过失都被归昝于效率(不一定能实现)。

​ —William A. Wulf [Wulf72]

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

​ —Donald E. Knuth [Knuth74]

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

​ 规则 1:不要进行优化。

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

​ —M. A. Jackson [Jackson75]

所有这些格言都比 Java 编程语言早了 20 年。它们告诉我们关于优化的一个深刻的事实:很容易弊大于利,尤其是如果过早地进行优化。在此过程中,你可能会生成既不快速也不正确且无法轻松修复的软件。

不要为了性能而牺牲合理的架构。努力编写 好的程序,而不是快速的程序。 如果一个好的程序不够快,它的架构将允许它被优化。好的程序体现了信息隐藏的原则:在可能的情况下,它们在单个组件中本地化设计决策,因此可以在不影响系统其余部分的情况下更改单个决策(详见第 15 条)。

这并不意味着在程序完成之前可以忽略性能问题。实现上的问题可以通过以后的优化来解决,但是对于架构缺陷,如果不重写系统,就不可能解决限制性能的问题。在系统完成之后再改变设计的某个基本方面可能导致结构不良的系统难以维护和进化。因此,你必须在设计过程中考虑性能。

尽量避免限制性能的设计决策。 设计中最难以更改的组件是那些指定组件之间以及与外部世界的交互的组件。这些设计组件中最主要的是 API、线路层协议和持久数据格式。这些设计组件不仅难以或不可能在事后更改,而且所有这些组件都可能对系统能够达到的性能造成重大限制。

考虑API设计决策的性能结果。 使公共类型转化为可变,可能需要大量不必要的防御性复制(详见第 50 条)。类似地,在一个公共类中使用继承(在这个类中组合将是合适的)将该类永远绑定到它的超类,这会人为地限制子类的性能(详见第 18 条)。最后一个例子是,在 API 中使用实现类而不是接口将你绑定到特定的实现,即使将来可能会编写更快的实现也无法使用(详见第 64 条)。

API 设计对性能的影响是非常实际的。考虑 java.awt.Component 中的 getSize 方法。该性能很关键方法返回 Dimension 实例的决定,加上维度实例是可变的决定,强制该方法的任何实现在每次调用时分配一个新的 Dimension 实例。尽管在现代 VM 上分配小对象并不昂贵,但不必要地分配数百万个对象也会对性能造成实际损害。

存在几种 API 设计替代方案。理想情况下,Dimension 应该是不可变的(详见第 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、线路层协议和持久数据格式时。当你完成了系统的构建之后,请度量它的性能。如果足够快,就完成了。如果没有,利用分析器找到问题的根源,并对系统的相关部分进行优化。第一步是检查算法的选择:再多的底层优化也不能弥补算法选择的不足。根据需要重复这个过程,在每次更改之后测量性能,直到你满意为止。

  • 0
    点赞
  • 0
    评论
  • 0
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

相关推荐
effective java 中文第三版 pdf文件下载 目录 01. 考虑使用静态工厂方法替代构造方法.md 02. 当构造方法参数过多时使用builder模式.md 03. 使用私有构造方法或枚类实现Singleton属性.md 04. 使用私有构造方法执行非实例化.md 05. 使用依赖注入取代硬连接资源(hardwiring resources).md 06. 避免创建不必要的对象.md 07. 消除过期的对象引用.md 08. 避免使用Finalizer和Cleaner机制.md 09. 使用try-with-resources语句替代try-finally语句.md 10. 重写equals方法时遵守通用约定.md 11. 重写equals方法时同时也要重写hashcode方法.md 12. 始终重写 toString 方法.md 13. 谨慎重写 clone 方法.md 14. 考虑实现Comparable接口.md 15. 使类和成员的可访问性最小化.md 16. 在公共类中使用访问方法而不是公共属性.md 17. 最小化可变性.md 18. 组合优于继承.md 19. 如使用继承则设计,应当文档说明,否则不该使用.md 20. 接口优于抽象类.md 21. 为后代设计接口.md 22. 接口仅用来定义类型.md 23. 优先使用类层次而不是标签类.md 24. 优先考虑静态成员类.md 25. 将源文件限制为单个顶级类.md 26. 不要使用原始类型.md 27. 消除非检查警告.md 28. 列表优于数组.md 29. 优先考虑泛型.md 30. 优先使用泛型方法.md 31. 使用限定通配符来增加API的灵活性.md 32. 合理结合泛型和可变参数.md 33. 优先考虑类型安全的异构容器.md 34. 使用枚举类型替代整型常量.md 35. 使用实例属性替代序数.md 36. 使用EnumSet替代位属性.md 37. 使用EnumMap替代序数索引.md 38. 使用接口模拟可扩展的枚举.md 39. 注解优于命名模式.md 40. 始终使用Override注解.md 41. 使用标记接口定义类型.md 42. lambda表达式优于匿名类.md 43. 方法引用优于lambda表达式.md 44. 优先使用标准的函数式接口.md 45. 明智审慎使用Stream.md 46. 优先考虑流中无副作用的函数.md 47. 优先使用Collection而不是Stream来作为方法的返回类型.md 48. 谨慎使用流并行.md 49. 检查参数有效性.md 50. 必要时进行防御性拷贝.md 51. 仔细设计方法签名.md 52. 明智审慎使用重载.md 53. 明智审慎使用可变参数.md 54. 返回空的数组或集合,不要返回 null.md 55. 明智审慎返回 Optional.md 56. 为所有已公开的 API 元素编写文档注释.md 57. 最小化局部变量的作用域.md 58. for-each 循环优于传统 for 循环.md 59. 了解并使用库.md 60. 若需要精确答案就应避免使用 float 和 double 类型.md 61. 基本数据类型优于包装类.md 62. 当使用其他类型更合适时应避免使用字符串.md 63. 当心字符串连接引起的性能问题.md 64. 通过接口引用对象.md 65. 接口优于反射.md 66. 明智审慎方法.md 67. 明智审慎进行优化.md 68. 遵守被广泛认可的命名约定.md 69. 只针对异常的情况下才使用异常.md 70. 对可恢复的情况使用受检异常,对编程错误使用运行时异常.md 71. 避免不必要的使用受检异常.md 72. 优先使用标准的异常.md 73. 抛出与抽象对应的异常.md 74. 每个方法抛出的异常都需要创建文档.md 75. 在细节消息中包含失败一捕获信息.md 76. 保持失败原子性.md 77. 不要忽略异常.md 78. 同步访问共享的可变数据.md 79. 避免过度同步.md 80. executor 、task 和 stream 优先于线程.md 81. 相比 wait 和 notify 优先使用并发工具.md 82. 文档应包含线程安全属性.md 83. 明智审慎的使用延迟初始化.md 84. 不要依赖线程调度器.md 85. 优先选择 Java 序列化的替代方案.md 86. 非常谨慎实现 Serializable.md 87. 考虑使用自定义的序列化形式.md 88. 保护性的编写 readObject 方法.md 89. 对于实例控制,枚举类型优于 readResolve.md 90. 考虑用序列化代理代替序列化实例.md
©️2020 CSDN 皮肤主题: 黑客帝国 设计师:白松林 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值