本篇博客以“SSD6-Exercise5:Cache Lab”为例,首先介绍存储器的层次结构,重点阐述了提高程序局部性的重要性,以及编写高速缓存友好代码的方法。
如果你了解系统是如何将数据再存储器层次结构中上上下下移动的,那么你就可以编写自己的应用程序,使得它们的数据项存储在层次结构中较高的地方,在那里CPU可以更快地访问到它们。
实际上,存储器系统 (memory system) 是一个具有不同容量、成本和访问时间的存储设备的层次结构。
CPU寄存器保存着最常用的数据,靠近CPU的小的、快速的高速缓存存储器 (cache memory) 作为一部分存储在相对慢速的主存储器 (main memory) 中数据和指令的缓冲区域。主存缓存存储在容量较大的、慢速磁盘上的数据,而这些磁盘常常又作为存储在通过网络连接的其他机器的磁盘或磁带上的数据的缓冲区域。
如上图所示,L1、L2、L3均为不同级别的高速缓存,本篇博客主要讨论的即为高速缓存(cache)对程序性能的影响。
这些思想围绕着计算机程序的一个称为局部性 (locality) 的基本属性。具有良好局部性的程序倾向于一次又一次地访问相同的数据项集合,或者倾向于访问邻近的数据项集合。
局部性通常有两种不同的形式:时间局部性 (temporal locality) 和空间局部性 (spatial locality) 。
在量化评价程序中的局部性时,还有以下的一些原则:
局部性好的程序更容易有较低的不命中率,而不命中率较低的程序往往比不命中率较高的程序运行得更快。
因此,从具有良好局部性的意义上来说,好的程序员总是应该试着去编写高速缓存友好 (cache friendly) 的代码。
下面是我们用来确保代码高速缓存友好的基本方法:
题目详情参见“SSD6-Exercise5:Cache Lab”。下面将题目中的几个重点特别说明:
未经局部性优化时,smooth()
与rotate()
函数的实现分别如下:
void smooth(int dim, pixel *src, pixel *dst) {
int i, j;
for(i=0; i<dim;i++) {
COPY(&dst[PIXEL(i,0,dim)], &src[PIXEL(i,0,dim)]);
COPY(&dst[PIXEL(i,dim-1,dim)], &src[PIXEL(i,dim-1,dim)]);
}
for(j=1; j<dim-1;j++) {
COPY(&dst[PIXEL(0,j,dim)], &src[PIXEL(0,j,dim)]);
COPY(&dst[PIXEL(dim-1,j,dim)], &src[PIXEL(dim-1,j,dim)]);
}
for(i=1; i<dim-1; i++) {
for(j=1; j<dim-1; j++) {
SMOOTH(&dst[PIXEL(j,i,dim)],
&src[PIXEL(j,i,dim)],
&src[PIXEL(j-1,i,dim)],
&src[PIXEL(j+1,i,dim)],
&src[PIXEL(j,i+1,dim)],
&src[PIXEL(j,i-1,dim)],
&src[PIXEL(j-1,i-1,dim)],
&src[PIXEL(j+1,i+1,dim)],
&src[PIXEL(j-1,i+1,dim)],
&src[PIXEL(j+1,i-1,dim)]);
}
}
return;
}
smooth()
分别对像素点(j, i)
及其周围的8个点进行复制。
void rotate(int dim, pixel *src, pixel *dst) {
int i, j;
for(i=0; i < dim; i++) {
for(j=0; j < dim; j++) {
COPY(&dst[PIXEL(dim-1-j,i,dim)], &src[PIXEL(i,j,dim)]);
}
}
return;
}
rotate()
将像素点(i, j)
复制到(dim-1-j, i)
处。
根据上文对局部性的介绍,再根据题目中所提供的线索,我们可以从以下的角度设计对高速缓存友好的代码:
void smooth(int dim, pixel *src, pixel *dst) {
int i, j;
for (i = 0; i < dim; i++) {
COPY(&dst[PIXEL(i, 0, dim)], &src[PIXEL(i, 0, dim)]);
COPY(&dst[PIXEL(i, dim - 1, dim)], &src[PIXEL(i, dim - 1, dim)]);
}
for (j = 1; j < dim - 1; j++) {
COPY(&dst[PIXEL(0, j, dim)], &src[PIXEL(0, j, dim)]);
COPY(&dst[PIXEL(dim - 1, j, dim)], &src[PIXEL(dim - 1, j, dim)]);
}
for (j = 1; j < dim - 1; j++) {
for (i = 1; i < dim - 1; i++) {
SMOOTH(
&dst[PIXEL(j, i, dim)],
&src[PIXEL(j, i - 1, dim)],
&src[PIXEL(j, i, dim)],
&src[PIXEL(j, i + 1, dim)],
&src[PIXEL(j - 1, i, dim)],
&src[PIXEL(j + 1, i, dim)],
&src[PIXEL(j - 1, i - 1, dim)],
&src[PIXEL(j + 1, i + 1, dim)],
&src[PIXEL(j - 1, i + 1, dim)],
&src[PIXEL(j + 1, i - 1, dim)]
);
}
}
return;
}
上述代码改变了i
与j
的层顺序,从而实现了较好的空间局部性与时间局部性。
void rotate(int dim, pixel *src, pixel *dst) {
int mat = 4;
for(int col = 0; col < dim; col += mat)
for (int row = 0; row < dim; row += mat)
for (int j = col + mat - 1;j >= col;j-- )
for (int i = row; i < row + mat; i++)
COPY(&dst[PIXEL(dim - 1 - j, i, dim)], &src[PIXEL(i, j, dim)]);
}
根据“Cache Structure: 16KB directed-mapped cahe, with 32-byte cache lines”的高速缓存结构,在这里采取步长为4的策略,并加深循环的层数以达到良好的空间局部性。
测试结果如下:
DEBUG: dimension=64.0
DEBUG: work=4096.0
DEBUG: dimension=128.0
DEBUG: work=16384.0
DEBUG: dimension=256.0
DEBUG: work=65536.0
DEBUG: dimension=512.0
DEBUG: work=262144.0
DEBUG: dimension=1024.0
DEBUG: work=1048576.0
Rotate: Version = Rotate Reference Naive Implementation!:
Dim 64 128 256 512 1024 Mean
hitrate 86.8 43.8 43.8 43.7 43.7
Incr. 1.00 1.00 1.00 1.00 1.00 1.00
DEBUG: dimension=64.0
DEBUG: work=4096.0
DEBUG: dimension=128.0
DEBUG: work=16384.0
DEBUG: dimension=256.0
DEBUG: work=65536.0
DEBUG: dimension=512.0
DEBUG: work=262144.0
DEBUG: dimension=1024.0
DEBUG: work=1048576.0
Rotate: Version = Exchanged-Matrix Row-wise Traversal of src:
Dim 64 128 256 512 1024 Mean
hitrate 87.1 81.2 81.2 80.9 81.0
Incr. 1.00 1.86 1.86 1.85 1.85 1.64
Best algo here: Exchanged-Matrix Row-wise Traversal of src, 1.640945
DEBUG: dimension=64.0
DEBUG: work=4096.0
DEBUG: dimension=128.0
DEBUG: work=16384.0
DEBUG: dimension=256.0
DEBUG: work=65536.0
DEBUG: dimension=512.0
DEBUG: work=262144.0
DEBUG: dimension=1024.0
DEBUG: work=1048576.0
Smooth: Version = Smooth Reference Naive Implementation!:
Dim 64 128 256 512 1024 Mean
hitrate 63.1 45.4 45.6 45.7 45.8
Incr. 1.00 1.00 1.00 1.00 1.00 1.00
DEBUG: dimension=64.0
DEBUG: work=4096.0
DEBUG: dimension=128.0
DEBUG: work=16384.0
DEBUG: dimension=256.0
DEBUG: work=65536.0
DEBUG: dimension=512.0
DEBUG: work=262144.0
DEBUG: dimension=1024.0
DEBUG: work=1048576.0
Smooth: Version = Exchanged-row-column Traversal of src:
Dim 64 128 256 512 1024 Mean
hitrate 63.1 63.8 64.2 64.4 64.5
Incr. 1.00 1.41 1.41 1.41 1.41 1.31
Best algo here: Exchanged-row-column Traversal of src, 1.314475
Program ended with exit code: 0
我们可以看到每个函数在处理各尺寸图像时cache的命中率,通过局部性的改良,程序的运行速度得到了极大的提升。
让最常见的情况运行得快。程序通常把大部分时间都花在少量的核心函数上,而这些函数通常把大部分时间都花在了少量的循环上。所以要把注意力集中在核心函数的循环上,而忽略其他部分。
在每个循环内部使缓存不命中数量最小。在其他条件,例如加载和存储的总次数相同的情况下,不命中率低的程序运行得更快。
注意:编译器将局部变量存储到寄存器中,因此循环内对局部变量的引用不需要任何加载或存储指令。
通过重新排列循环以提高空间局部性:降低高速缓冲的不命中率。例子(求两个矩阵的乘积)
分块的大致思想是将一个程序中的数据结构组织成称为块(block)的组块(chunk)。这里的“块”指的是一个应用级的块,不是高速缓冲块。这样构造程序,使得能够将一个块加载到L1高速缓存中,并在这个块中进行所需的所有的读和写,然后丢掉这个块,加载下一个块,以此类推。
[1] 《深入理解计算机系统》(第3版).Randal E. Bryant, David R.O’Hallaron 著.
[2] 博客:程序设计原则——局部性原理.
[3] 博客:SSD6-Exercise5:Cache Lab