<audio id="audio" title="19 | 性能优化六大原则：三要三不要，快速有效地进行优化" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/06/01/06d0b1ac11cea369cec367f878074b01.mp3"></audio>

你好，我是庄振运。

今天我们进入了专栏的新模块：性能优化。在这个模块里，我会先从“性能优化的六大原则”开始讲起，然后再为你讲解实践中普遍采用的十个性能优化策略，并且分别针对CPU、系统、存储以及跨层这几个领域，讲讲具体的优化案例。

我们今天先探讨性能优化的原则。在讲具体原则之前，我想先给你讲一个有趣的往事。

我曾经负责过一个存储服务的性能优化和容量效率。那个服务的容量需求很大，但它的最大性能瓶颈不是CPU，而是存储的空间。

所以，虽然有很多人给我们各种建议，让我们花时间做CPU优化，我都尽量挡了回去。因为我知道CPU不是最大问题，所以坚持不懈地通过各种途径优化数据大小，甚至以牺牲CPU为代价。最后的结果很好，大幅度地降低了那个服务的容量需求。

在这个性能优化的场景，我们遵循了一个原则，那就是**优先优化最大性能瓶颈**。这其实就是马上要讲到的“三要”原则中的第一个。

## 性能优化的原则概述

在实际的性能优化中，我们需要考虑的因素很多，也经常需要在多个角度和目标间做一些平衡和取舍。为了帮助你把握这些，我个人总结出了六条原则，我把它们概括为：“**三要，三不要**”。

<img src="https://static001.geekbang.org/resource/image/bc/bb/bc08d73063d641b231db155d7ffe26bb.jpg" alt="">

- 三个“要”原则是：要优先查最大的性能瓶颈，性能分析要确诊性能问题的根因，性能优化要考虑各种的情况。
- 三个“不要”的原则是：不要做过度的、反常态的优化，不要过早做不成熟的优化，不要做表面的肤浅优化。

接下来，我们一个个地讲一下这六个优化原则。

## 三个“要”的原则

你可能已经发现了，这三个“要”的原则之间，其实是步步递进的关系。也就是首先需要查找最大性能瓶颈，然后确诊性能瓶颈产生的原因，最后针锋相对地提出最好的解决方案。

而这个最优解，往往是在考虑各种情况之后提出来，并最终被选中的。

### 要优先查最大的性能瓶颈

任何一个应用程序或者系统，总会有很多地方可以优化。

可是你知道要从何处下手吗？

其实你可以参考古人的观点，唐代的诗人杜甫说过：“射人先射马，擒贼先擒王”。会打仗的将军，一定会从“最值得打的地方”开始，性能优化工作也是如此。

我们永远都要优先从**最大的性能瓶颈**入手。

一般来讲，如果找到最大的性能瓶颈，并且解决了它，那这个系统的性能会得到最大的提升（参见[第4讲](https://time.geekbang.org/column/article/174462)帕累托法则）。反之，如果不解决最大的性能瓶颈，反而退而求其次，去解决了其他的性能问题，整个系统的性能或许会更高一些，但是提升的程度往往是非常有限的。

比如，一个应用程序的最大性能瓶颈是CPU的使用率太高；而其他种类型的资源，比如网络和存储，都很有富余。这种情况下，如果你去优化网络和存储方面，显然是不能大幅度地提升整体性能的。

不过这里有个稍微特殊的情况，就是**内存**。因为内存的分配和回收也会消耗一些CPU资源，如果这时去优化内存使用，很多时候，的确会帮助你降低CPU使用率，不过降低的幅度一定是很有限的。

归根结底，要降低CPU的使用率，**最有效的方法是做性能分析和剖析，找出程序中使用CPU最多的地方**，然后对症下药地做优化。

### 要确诊性能问题的根因

我们前面讲过，程序和系统如果在某个地方有性能瓶颈，肯定是这个地方的资源不够用了，不管是CPU、内存还是网络（参见[第14讲](https://time.geekbang.org/column/article/182915)）。

所以，当我们确定了最大的性能瓶颈后，就需要对这一性能瓶颈做彻底的性能分析，找出资源不够使用的原因，也就是考察使用资源的地方。

一种资源被使用的地方往往有好几个，我们需要一个一个地去分析考虑。只有彻底分析了各种使用的情况，才能进一步找出最主要的，也是最可能优化的原因，对症下药。

有些资源使用的原因也许是完全合理的。对这些合理的使用，有些或许已经仔细优化过了，很难再做优化。而另外一些则有可能继续优化。对资源的不合理使用，我们就要尽量想办法去掉。

对于需要优化的地方，我们就需要进一步考虑**优化工作的投入产出比例**，就是**既考虑成本，也考虑带来的好处**。因为有些情况下，虽然你可以去优化，但获得的收益并不大，所以不值得去做。

另外要提醒你的是，确诊性能问题的原因有时候非常困难，需要做多方面的性能测试、假设分析并验证。

比如CPU的使用，有操作系统的原因，有应用程序的原因，但也有些CPU的问题，是在非常边缘的场景下才发生的。为了暴露问题，我们经常需要创造特殊的场景来重现遇到的性能问题。

### 要考虑各种情况下的性能

性能问题确诊原因后，我们就可以进入下一步，找解决方案了。一般说来，找一个解决方案并不难，甚至找好几个方案也不难；但是找出一个好的、最优的解决方案是真心不容易。

为什么这么说呢？

因为实际生产环境很复杂，而且会出现各种各样的特殊场景。

**针对某个具体场景提出的一个解决方案，多半并不能适应所有的场景。**

所以，对提出的各种方案进行评估时，我们必须考虑各种情况下这个方案可能的表现。如果一个方案在某些情况下会导致其他严重的问题，这个方案或许就不是一个好的方案。

但同时你也需要意识到，任何解决方案都有长短，有Tradeoff。如果苛求一个能在所有场景下都最优的解决方案，往往是缘木求鱼，是不现实的。

比如一种优化方案，可以让平均响应时间最小，但高百分位比较高。另外一种优化方案正好相反。那我们就需要考虑对自己来说哪种指标更重要。也就是说，我们经常需要在**不同性能指标间权衡**，以找到一个最优解能**达到总体和整体最优**。

这就需要我们**有一个整体的意识和判断**。

或许一个方案并不能面面俱到，在有些场景下性能不好。但是不同场景的出现概率不同，对其它模块造成的影响也不一样，并且最终客户的体验也不尽相同。这些因素都要考虑到取舍的决策过程中。

综合以上因素，在实际的优化过程中，我们经常会**反复权衡利弊和取舍来做最终决定**。

## 三个“不要”的原则

讲完了三个“要”的原则，我们接着来看三个“不要”的原则。

### 不要过度地反常态优化

性能优化的目标，是追求**最合适的性价比**或**最高的投入产出比**，在满足要求的情况下，尽量不要做过度的优化。过度的优化会增加系统复杂度和维护成本，使得开发和测试周期变长。虽然性能上带来了一定程度的提升，但是和导致的缺点来比，孰轻孰重尚不可知，需要仔细斟酌，衡量得失。

我的建议是，**根据产品的性能要求来决策**。

在设计产品时，我们对产品的性能会有一定的要求，比如吞吐量，或者客户响应时间要达到多少多少。如果达不到这个既定指标，就需要去优化。反之，如果能满足这些指标，那么就不必要花费太多时间精力去优化。

比如，我们要设计一个内部查询系统，预计最多只有一百个人同时在线使用的话，就完全不用按照百万在线用户的目标去过度优化。

更重要的是，多数的优化方法是并不是完美无缺的，是有缺点的，尤其是可能会对系统设计的简化性，对代码的可读性和可维护性有副作用。如果系统简化性和代码可读性更加重要，当然就更不能过度优化。

### 不要过早的不成熟优化

要体会这一原则，我们先引用著名计算机科学家高德纳（Donald Knuth）的一段话：“现实中的最大问题是，程序员往往花太多时间，来在错误的地方和错误的时间来试图提高效率和性能。过早的优化，是编程中所有邪恶和悲剧（或至少是大多数邪恶和悲剧）的根源。”

<img src="https://static001.geekbang.org/resource/image/69/a7/6964528af8b4212dfa2291749b7415a7.png" alt="">

你只要稍微思考一下高德纳的话，就会发现，这句话在很多场景下都是很有道理的。

比如，在敏捷开发过程中，尤其是在面对一个全新的产品时，在业界没有先例和经验可遵循的情况下，最看重的特点是快速的迭代与试错，“尽快推出产品”是最重要的。这时，过早的优化很可能优化错地方，也就是优化的地方并非真正的性能瓶颈，因此让“优化工作”成为了无用功。而且，越早的优化就越容易造成负面影响，比如影响代码的可读性和维护性。

我个人认为，**如果一个产品已经在业界很成熟**，大家非常清楚它的生产环境特点和性能瓶颈，那么**优化的重要性可以适当提高**。否则的话，在没有实际数据指标的基础上，为了一点点的性能提升而进行盲目优化，的确是得不偿失的。

### 不要表面的肤浅优化

性能优化很忌讳表面和肤浅的优化，也就是那种“头痛医头，脚痛医脚”的所谓“优化”。如果对一个程序和服务没有全局的把握，没有理解底层运行机制，任何优化方案都很难达到最好的优化效果。

比如，如果你发现一个应用程序的CPU使用率并不高，但是吞吐率上不去，表面的优化方式可能是增大线程池来提升CPU使用率。这样的简单“优化”或许当时能马上看到效果，比如吞吐率也上去了，但是如果你仔细想想，就会发现如此的表面优化非常有问题。

这样的情况下，线程池开多大最合适？需不需要根据底层硬件和上层请求的变化而对线程池的大小调优呢？如果需要，那么手工调整线程池大小就是一个典型的“头痛医头”的优化。

为什么呢？

因为部署环境不会一成不变，比如以后CPU升级了，核数变多了，你怎么办？再次手工去调整吗？这样做很快会让人疲于奔命，难以应付，并且很容易出错。

对这样的场景，正确的优化方式，是彻底了解线程的特性，以优化线程为主。至于线程池的大小，最好能够自动调整。千万别动不动就手工调优。如果这样手工调整的参数多了，就会做出一个有很多可调参数的复杂系统，很难用，也很难调优，很不可取。就比如我们都熟悉的JVM调优，有上千个可调参数，非常被人诟病。

## 总结

唐朝的名相魏征说过：“求木之长者，必固其根本”。意思是说，如果要一棵树长得高，必须让它的根牢固。否则的话，正如魏征自己所说：“根不固而求木之长”，一定是“知其不可”。根基不牢固，就不可能长成参天大树。

<img src="https://static001.geekbang.org/resource/image/b0/e9/b07a06f7da4768d7398e1a174326cce9.png" alt="">

同理，对现代互联网的服务和系统来说，性能问题是根本的问题。如果不知道系统的性能瓶颈，查不出性能根因，不知道如何解决，无法做合理的优化，这个服务和系统一定不会高效。

这一讲我们总结了六个性能优化的原则，这些原则的终极目的，就是找出性能的最大瓶颈，查出根因，并作做相应的最优优化，从而让我们的系统这棵树长高、长大。

## 思考题

- 今天介绍的六大原则里面，你认为哪个原则最重要？为什么？
- 你过去的工作中有没有因为没有遵循这几个原则而吃后悔药的时候？比如你在选择一个数据结构的具体实现时（比如Set），有没有考虑各种场景下的性能？

欢迎你在留言区分享自己的思考，与我和其他同学一起讨论，也欢迎你把文章分享给自己的朋友。
