读书笔记(十六) 《C++性能优化指南》一

这本书给我的感受是,有技巧有细节也有许多不足,作者介绍了大部分程序上性能优化的方案和思路,也从原理上讲了性能问题的根本原理,但没能做得通俗易懂、深入浅出,书本有几处地方在故弄玄虚以及凑字数,也许是我的功力不足没能理解,这部分无法理解的杂乱无章的内容,可以留到以后再慢慢回顾。

整本书其实并不是针对C++写的,而是面向程序执行而效率写的,其他语言也一样通用。我结合我的经历和经验写下我的理解,以及我从书中学到的知识,或许过几年回头看会是另一番情景,但现在我需要把它们拎出来总结一下。

作者是一个有30多年编程经历的人,对编程依然保持的狂热兴趣,注意,他从未去过微软、谷歌等知名公司,这本书就是在这样的一个前提下写下的。

作者认为我们在优化程序的时候,很多时候都是靠‘猜测’而不是实际的去测试,这是一个比较大的问题。实际上我们并不知道某段程序是否有性能问题,因为有可能编译器已经将它优化了,或者某段程序在我们优化后是否真的有性能提升,因为很多时候我们只是肉眼去代码或是用脑袋去猜,不知道性能问题出在哪里就花费很多时间去优化这是不行的。而且我们也不能因为优化程序性能而破坏了程序的稳定性,如果从中制造出Bug导致产品崩溃那是非常得不偿失的。

另外他提出了一个比较重要的理念,即大部分性能问题在代码层面上的分布都是‘90/10规则’,也就是说,90%的性能问题出在10%的代码上。因此为了我们在做性能优化时提高效率,应该重点去找出这影响90%性能问题的那10%的代码,它们就是性能问题的重点。不过这10%的代码并不是集中在某处,而是分散在各个模块中,需要我们去找出来,因此按照我的经验和他的理念来理解性能优化的90/10规则,是说我们需要改动的代码远比整体代码要少的多,而我们必须精准的找出这部分少数代码并优化它。

原书内容比较繁杂,我又重新归类,我把它归类为,计算机执行原理、性能测试、字符串问题、算法、内存分配、热点代码、IO、并发,这八个方面。下面就让我们来讲讲我从书本中学到的对性能优化的理解。

计算机执行原理

代码从被编译到成为可执行文件也就是机器码,这个过程就是一个从本文字符串翻译成机器码的过程,当我们执行它们的时候,它可机器码被放入了内存,内存中也有分块,包括数据段、栈段、指令段。

CPU在执行指令时是从内存中将指令送入CPU的,而执行指令的速度通常比读取内存的快很多,因此读取指令也成为了瓶颈的一种。CPU在读取指令时也并不会一行一行的读取,因为这样效率太差,取而代之的是它会把一大块内容读取到高速缓存从而加快执行速度,指令会顺序执行直到结束或有跳转。

而内存芯片也有自己的工作原理,它相当于另一个CPU,它只有在顺序访问时才能在一个周期内完成,而访问一个非连续的位置则会花费更多周期。

这里就涉及到了内存在访问时的形式,每次访问内存都是以某个大小为单位,例如x86机器,每次访问内存时都是以4个字节为单位访问,一个int整数为4个字节需要一次访问,但如果这个int整数内存没有对齐,那么可能就需要访问两次才能获得这个值,因为构成这个内存的物理结构可能是垮了两个物理内存字。

不过请注意,现代编译器都会默认对对象和数据结构做内存对齐操作,除非我们告诉编译器某个数据结构不做内存对齐。对齐时编译器也会优化内存结构让高速缓存命中率提高,关于class的内存布局我们在《深度探索C++对象模型》总结中有详细的讲解。

作者没有细说关于不对齐时内存访问的来龙去脉,不过我们来举个例子:

[-][-][x][x][x][x][-][-]

例如上面这个非对齐的内存空间,‘x’表示某个int变量占用的4个字节,‘-’表示其他,当CPU读取这个int整数时,其实是先读取

[-][-][x][x]

再读取

[x][x][-][-]

拼接完成后为

[x][x][x][x]

最后交给寄存器。实际上访问非内存对齐并没有我们想象的那么简单,一个内存实际上有很多个内存芯片共同组成。为了提高访问的带宽,通常会将存储地址分开放在不同的芯片上,例如上面位置0,1,2,3,这4个byte分别存放在芯片1,芯片2,芯片3,芯片4中,当需要它们时,可以一次性全部读取,即如下:

偏移量/芯片存储空间
   x1 x2 x3 x4
0 [-][x][x][x]
1 [x][-][-][-]
2 [-][-][-][-]
3 [-][-][-][-]
4 [-][-][-][-]

图中x为要读取的数据,每一列为一个芯片负责的空间,每一行为一个偏移量。这就是说,内存实际上并不是完全以连续byte形式组织的,而是以偏移(offset)量给出具体地址。当我们读取[0][1][2][3]这4个byte数据时可以一次性读取,但如果从1开始读取1,2,3,4时就要多一次偏移(offset)的操作,即先让4个芯片读取偏移量为0时的数据,再让它们读取偏移量为1的数据。

为了改善内存速度,高速缓存被大量运用,即我们说的L1、L2、L3、L4四级高速缓存,它们每一层的速度大约是下一层的10倍左右。当执行单位需要的内容不在高速缓存中时,需要从内存中加载数据到高速缓存中,并同时将一部分内容舍弃以换取足够的空间,通常会选择放弃的数据都是最近被使用频率比较低的数据。在读取一个不在高速缓存中的数据时通常会将临近的数据也被缓存起来,从访问概率上来说做了加速了数据访问,即概率上来说临近的数据访问概率比较高的特点。

这意味着频繁被访问的数据和频繁被访问的周围附近的数据都会因为高速缓存而加速。不仅仅是变量数据,机器指令也是数据的一种,超远的if和goto跳转以及远地址function函数调用同样会让高速缓存失效,其原理是执行指令地址从一处跳到另一处导致执行指令不连续。

内存的访问也会有不够的时候,这时虚拟内存带来了很大的便利,但也给性能带来了很大影响。在虚拟内存机制中,当内存不够时需要借用磁盘空间来扩充,这让内存制造出拥有充足物理空间的假象,将使用频率小的内存数据作为文件存放在磁盘上,当使用时再读区进内存同时更换部分内存到硬盘上,我们常称它们为内存swap操作。由于swap操作很费时,因此检测swap次数也常被纳入性能监控中。

通常我们一个操作系统中有多个程序需要同时内存访问,而内存总线就只有一个,内存芯片必须一个个去完成CPU分配给它的任务,有时甚至是经常不连续的内存访问,由此看来,内存的读写负担是相当重的。如果未来有更多的处理器内核增加,而内存接口和读取速度没有增加的话,那么其实这些内核对性能的改善效果也是趋于递减的,因为虽然有多个CPU来处理指令,但内存并没有被加速。

接着我们来看看线程和进程如何影响执行效率,由于操作系统会执行一个线程很短时间然后将上下文切换到其他线程或进程。导致在切换时会浪费掉很多时间,操作系统需要暂停当前的线程并且保存处理器中的寄存器到内存,然后为即将被执行的线程加载之前保存过的寄存器。如果新的线程的数据不在高速缓存中,那么还需要从内存中加载数据到高速缓存,因此上下文切换的代价比我们想象的高。

这里做个小结,以上讲的都是些计算机执行原理,我们需要明白的原理才能真正明白优化背后的逻辑。首先内存效率没有我们想象那么快,非对齐内存会多一次开销(编译器通常都有做内存对齐),指令远距离跳转和内存远距离访问都会让高速缓存命中丢失,频繁访问的数据和附近的数据会比较快,虚拟内存扩充了内存但swap时性能开销很大,多个进程和线程争夺内存使用权是性能瓶颈之一,太多线程和进程上下文切换代价较大,会导致执行效率降低。

性能测试

性能测试对于性能优化来说是关键的关键,就像作者提到的那样,我们不能靠猜来判断哪些代码需要优化,或者代码执行效率提升了多少不能由某个人说了算。

那些具有最让人折服的优化技巧的开发人员都会系统地完成如下步骤:


1.测试出哪些地方是可优化的,做出预测并记录预测。

2.保留优化的代码记录

3.用测试工具进行测量优化前后的数据对比

4.保留实验结果并做详细的笔记。

以上列的四项步骤是性能优化过程中必须不断实践的技能,现实中多数开发人员都想当然的去优化代码,而不是按照上面的方式有条不紊的进行优化,这是优化过程中最糟糕的一点,即不知道自己该从哪里开始优化,优化结束时不知道是否真的优化了优化了多少,有可能更加糟糕,过了段时间甚至记不起来优化了什么。

除了实际的去测量和记录,我们在性能测试时要注意哪些关键点呢?作者给出了自己的经验。

1.测量程序的启动时间,执行时间,退出时间。

通常人们总是忘了启动和退出时间,这导致部分测量范围不够或者测量不准确。

2.测量的数据和环境必须是可重复的。

只有两个数据和环境是可重复的,才有可能让两次测量在同一个标准中进行。

3.测量必须有一个标准和一致的范围。

如果前后两次测量的环境、测量内容、测试的持续时间不一致,则测量出来的数据是无效的,这种情况下的任何优化数据都是可笑的。

4.测量数据通常都是波动的,没有不波动的测量数据。

因此我们需要通过反复测量多次给出平均值的方式来确定最后的数据。

5.其他进程会影响测量结果。

关闭其他会导致影响的进程,或者提高测量进程为最高级别。

6.测量工具很重要

测量工具包括类似Stopwatch方式的打点,抓取堆栈调用时间,内存分配,内存快照等方式。

7.分析代码和测量运行时间是帮助找出可优化代码的两种有效途径。

只分析代码是行不通的,只测量运行时间也不可行。要分析代码与测量相互迭代,分析后测量,测量后分析,以此方式不断找出可优化的代码。

开发人员需要向同事和领导展示他们在性能优化中取得的进展,我们需要精准的测量和详细的记录,如果凭直觉进行优化,也不发表结果,或者发表了结果也会遭到质疑,这是因为他们分不清你到底是在用高度专业的直觉进行优化还是只是在碰运气。

字符串问题

这把字符串问题单独拎出来说是因为字符串问题比较大,也比较隐性,常常容易引起性能问题。字符串在概念上很简单,它就是一个字符数组,但是想要实现高效的字符串却非常不容易。

这周太忙,暂时写到这里,下周继续…

· 读书笔记

感谢您的耐心阅读

Thanks for your reading

  • 版权申明

    本文为博主原创文章,未经允许不得转载:

    读书笔记(十六) 《C++性能优化指南》一

    Copyright attention

    Please don't reprint without authorize.

  • 微信公众号,文章同步推送,致力于分享一个资深程序员在北上广深拼搏中对世界的理解

    QQ交流群: 777859752 (高级程序书友会)