我们知道 CPU 访问内存的速度,比访问 CPU Cache 的速度慢了 100 多倍,所以如果 CPU 所要操作的数据在 CPU Cache 中的话,这样将会带来很大的性能提升。访问的数据在 CPU Cache 中的话,意味着
缓存命中
,缓存命中率越高的话,代码的性能就会越好,CPU 也就跑的越快。
于是,「如何写出让 CPU 跑得更快的代码?」这个问题,可以改成「如何写出 CPU 缓存命中率高的代码?」。
之所以有这么大的差距,是因为二维数组
array
所占用的内存是连续的,比如长度
N
的指是
2
的话,那么内存中的数组元素的布局顺序是这样的:
形式一用
array[i][j]
访问数组元素的顺序,正是和内存中数组元素存放的顺序一致。当 CPU 访问
array[0][0]
时,由于该数据不在 Cache 中,于是会「顺序」把跟随其后的 3 个元素从内存中加载到 CPU Cache,这样当 CPU 访问后面的 3 个数组元素时,就能在 CPU Cache 中成功地找到数据,这意味着缓存命中率很高,缓存命中的数据不需要访问内存,这便大大提高了代码的性能。
而如果用形式二的
array[j][i]
来访问,则访问的顺序就是:
你可以看到,访问的方式跳跃式的,而不是顺序的,那么如果 N 的数值很大,那么操作
array[j][i]
时,是没办法把
array[j+1][i]
也读入到 CPU Cache 中的,既然
array[j+1][i]
没有读取到 CPU Cache,那么就需要从内存读取该数据元素了。很明显,这种不连续性、跳跃式访问数据元素的方式,可能不能充分利用到了 CPU Cache 的特性,从而代码的性能不高。
那访问
array[0][0]
元素时,CPU 具体会一次从内存中加载多少元素到 CPU Cache 呢?这个问题,在前面我们也提到过,这跟 CPU Cache Line 有关,它表示
CPU Cache 一次性能加载数据的大小
,可以在 Linux 里通过
coherency_line_size
配置查看 它的大小,通常是 64 个字节。
也就是说,当 CPU 访问内存数据时,如果数据不在 CPU Cache 中,则会一次性会连续加载 64 字节大小的数据到 CPU Cache,那么当访问
array[0][0]
时,由于该元素不足 64 字节,于是就会往后
顺序
读取
array[0][0]~array[0][15]
到 CPU Cache 中。顺序访问的
array[i][j]
因为利用了这一特点,所以就会比跳跃式访问的
array[j][i]
要快。
因此,遇到这种遍历数组的情况时,按照内存布局顺序访问,将可以有效的利用 CPU Cache 带来的好处,这样我们代码的性能就会得到很大的提升,
如何提升指令缓存的命中率?
提升数据的缓存命中率的方式,是按照内存布局顺序访问,那针对指令的缓存该如何提升呢?
我们以一个例子来看看,有一个元素为 0 到 100 之间随机数字组成的一维数组:
接下来,对这个数组做两个操作:
第一个操作,循环遍历数组,把小于 50 的数组元素置为 0;
第二个操作,将数组排序;
那么问题来了,你觉得先遍历再排序速度快,还是先排序再遍历速度快呢?
在回答这个问题之前,我们先了解 CPU 的
分支预测器
。对于 if 条件语句,意味着此时至少可以选择跳转到两段不同的指令执行,也就是 if 还是 else 中的指令。那么,
如果分支预测可以预测到接下来要执行 if 里的指令,还是 else 指令的话,就可以「提前」把这些指令放在指令缓存中,这样 CPU 可以直接从 Cache 读取到指令,于是执行速度就会很快
。