[quality] 03 - cache: system level optimization
有本书看上去不错:《C++应用程序性能优化::第一章C++对象模型》学习和理解
这里比较关心的是:深度优化,系统级优化,直到高缓级别。想想就爽死了。
牲口才能精通的文档:http://gcc.gnu.org/onlinedocs/gcc/Optimize-Options.html
基础知识
软件在架构优化、算法优化、代码优化之后,往往会进入系统级优化,比如Cache优化。
现代CPU的Cache一般分为三级:
- 一级,Cache生产成本最高,容量最小,但是速度最快,处理器访问一级Cache中的数据一般只需要3~5个指令周期,一级Cache又分为数据Cache和指令Cache,分别缓存数据和指令;
- 二级,Cache不区分数据和指令,容量较大,速度较慢;
- 三级,Cache也称LLC(Last Level Cache),容量最大,而速度更慢。
在多核时代,一般一颗CPU中,每核都有独立的一级和二级Cache,而多核之间共享三级Cache。如下图:

Cache 机制
【Cache 读写机制】
Cache和内存之间按块进行数据交换,块大小为内存的一个存储周期能访问到的数据长度,一般为64字节,又称为Cache Line或缓存行。Cache和内存的关联方式以及Cache回写策略等,对于上层软件优化来说,可以暂不考虑。
Cache之所以能提高系统性能,主要在于程序执行具有局部性现象,包括时间局部性和空间局部性。
- 时间局部性是指,程序即将用到的数据和指令可能就是目前正在使用的数据和指令。利用时间局部性,可以将当前访问的数据和指令存放到Cache中,以便将来使用,比如C++语言中的for、while循环、递归调用等。
- 空间局部性是指,程序即将用到的数据和指令可能与目前正在使用的数据和指令在地址空间上相邻或者相近。
利用空间局部性,可以在处理器处理当前数据和指令时,把内存中相邻区域的指令/数据读取到Cache中,以备将来使用,比如数组访问、顺序执行的指令等。局部性原理也符合80-20原则,即程序20%的代码占用了处理器百分之八十的执行时间,占用了80%的内存。Cache预取就是根据局部性原理,预测数据和指令使用情况,并提前载入到Cache中,这样,当数据/指令需要被使用时,就能快速从Cache中获取到,而不需要访问内存。
现代CPU基本都是多核架构,多个核心有各自的L1/L2 Cache,当多个核心需要修改同一CacheLine时,需要在多核之间进行Cache同步,以保证Cache 一致性,Intel CPU使用MESIF协议来实现Cache一致。
总结一下,Cache主要特点有:
- ■ 容量小、速度快,可以使用下面命令在Linux上查询Cache容量:

- ■ 按行存取:一般缓存行大小为64B,可以使用下面命令在Linux上查询Cache Line大小:
-

- ■ 硬件预取:根据程序执行的局部性原理,CPU中的预取部件会对Cache进行预取,当前正在访问的数据/指令,以及周围区域的数据/指令都会被提前放入Cache。
- ■ 缓存一致性:多核同时修改同一行Cache时,需进行Cache同步。
- ■ 同时缓存指令和数据。
- ■ TLB:CPU中还有一种Cache叫做TLB(Translation Lookaside Buffer),专门用来缓存虚拟地址到物理地址的映射,加快地址解析。
编写Cache友好型代码
在开发频繁访问内存的应用时,Cache对应用的性能影响很大,这就要求我们尽量编写Cache友好型代码,下面列举一些使用经验:
【CPU 绑定】
当进程被调度到其他核心或其他CPU后,Cache中的指令和数据都将无效,TLB缓存也将失效。在极速场景下,需要避免这种情况,可以通过设置CPU的线程亲缘性来减轻,也称为CPU绑定。还可以进一步通过配置isolcpus隔离CPU,降低CPU被调度的概率。需要注意的是,一般情况下不推荐绑定CPU。

上面代码中,将当前线程绑定到了xxx编号的CPU上。
【系统调用】
在系统调用时,将导致用户态和内核态切换,不仅整个切换过程耗时,而且由于用户态和内核态执行的代码不一样,会导致Cache失效。可以通过减少或者合并系统调用,来降低影响。
比如写文件系统调用为write(),同时系统也提供了writev(),实现一次写入多块数据,还提供了文件映射内存mmap(),该方式可以基本避免系统调用。
【数据集中存放】
Cache和内存速度差异这么大,所以要尽量少访问内存,可以的话,尽量将数据合并存放在一起,利用Cache预取,一次将尽可能多的数据从内存加载到Cache。
比如金融业务中通常存在用户、订单、资金信息,每次处理订单时,都需要查询用户、资金信息,如果设计数据结构时,能将这些信息在内存中连续存放,就可以实现访问一次内存获取到所有数据,避免多次访问零碎内存。
【Cache Line 对齐】
默认情况下,内存地址会按照数据大小对齐,这样有些数据结构可能会跨Cache行存放,占用多行Cache,从而影响性能,所以在一些关键数据结构上,可以手动设置成按Cache Line对齐,避免占用多行Cache。按Cache Line对齐代码示例如下:

在上面代码中,定义了两个宏CACHE_LINE_SIZE,和CACHE_LINE_ALIGN。
CACHE_LINE_SIZE 表示Cache行大小,大多数情况下为64。
CACHE_LINE_ALIGN 是根据各平台编译器提供的语法,定义的按CACHE_LINE_SIZE对齐的宏。接着又定义了XXX类型的变量x,XXX为任意数据类型,可以为基础类型比如int,也可以是自定义类型。这样定义以后,变量x的地址就是64的整数倍。
如果采用C++11语法就更简单了,C++11提供了关键字alignas来设置数据的对齐方式:alignas(CACHE_LINE_SIZE) XXX x。
韭菜花推荐:不错的讲解,有例子:从硬件到语言,详解C++的内存对齐(memory alignment)
【数组按行访问】
C++中数组在内存中是按行存放的,因此在访问多维数组时,尽量按行访问,避免按列访问。因为按列访问,会导致跳跃式访问内存,数据量大的时候,会导致频繁的Cache换入换出,对Cache很不友好。
【Cache 伪共享】
不同线程的数据尽量放到不同的Cache Line,避免多线程修改同一行Cache,导致Cache需要在多核之间进行同步。由于这种共享不是程序本意,而是由于底层Cache按块存取机制导致的,又称为Cache 伪共享。
比如,在编写多线程程序时,会有一些线程指标统计的工作,为了避免线程竞争,一般会定义一个数组,每个线程修改其中一个元素,需要统计信息时,将所有元素相加得到结果。(其实就是每个都给予一个独立的空间)代码示例:

如上,将变量counter设置为按Cache行大小对齐,这样数组threads_info的每个元素都会占用单独的一行Cache,多个线程修改各自的counter,就不会存在Cache行竞争,避免Cache同步。
【软件预取】
在某些场景中,可以在代码里,通过CPU提供的预取指令,进行软件预取,更高效的使用缓存。
- Windows下可以使用VC++提供的 _mm_prefetch 函数,
- Linux下可以使用GCC提供的 __builtin_prefetch 函数。
韭菜花推荐:数据预取 __builtin_prefetch()
【指令预取】
程序中的热点函数尽量放在一起,方便指令预取,减小指令缓存的占用,同时,模块间的函数调用,可以通过调整编译器链接顺序,来调整指令位置。
另外,如果程序经常执行跳转指令,将不利于指令预取,我们可以采用软件分支预测方法,帮助编译器生成更优化的指令。Linux下一般使用如下代码实现:

likely和unlikely是C++宏,likely宏的意思是告诉编译器之后的分支执行概率较大,unlikely刚好相反。在上例中,因为使用了unlikely,编译器认为if分支执行概率小,因此在生成指令时,会将else分支的指令放在前面,以减少程序执行时的指令跳转,使指令尽可能顺序执行。
【高精度计时】
在做程序优化时,需要一个高精度时间测量方法,来避免测量本身带来的开销和干扰。Intel CPU提供了rdtsc和rdtscp指令,用于获取CPU时钟周期,其中rdtscp能避免乱序执行问题,保证在rdtscp之前的所有程序指令执行完以后,才会被执行,从而能更精确的获取时间。利用这些指令封装的获取时间函数定义如下:

在实际使用中,函数rdtscp1()需要放在被测代码之前,获取程序起始执行时间,函数rdtscp2()放在被测代码之后,用于获取结束时间。函数汇编指令中的CPUID用于指令序列化,避免前面或后面的指令由于乱序执行而提前或推迟执行,从而影响到测试代码。需要注意的是,这两个函数返回的是CPU时钟周期,在换算为时间时,还需要除以CPU的频率。另外这些函数返回的值,是CPU加电后的线性时间,而不是真实时间。
补充与参考:Linux 内核中的 GCC 特性【貌似也没说啥】
How to optimize C and C++ code in 2018
读后感以及总结。
非常好的演讲:Understanding Compiler Optimization - Chandler Carruth - Opening Keynote Meeting C++ 2015
HHVM team made an impressive investigation to find the culprit: aggressively inlined memcpy. The code of the memcpy function size was 11KB on their platform and caused a cache thrashing. Useful data was evicted from the CPU cache by this code everywhere in a program.
inline, static 的使用。

浙公网安备 33010602011771号