几种通过降低Cache失效率来提升程序性能的方法
当程序访问多个数组时,经常会出现有些数组按行访问,有些数组按列访问的情况。以矩阵的乘法为例, C = A × B C=A\times B C=A×B ,经典的计算矩阵乘法的算法如下:
void mult() {
for(int i=0;i<N;i++) {
for(int j=0;j<N;j++) {
double res=0.0;
for(int k=0;k<N;k++) {
res += (matrix_a[i][k]*matrix_b[k][j]);
}
matrix_c[i][j] = res;
}
}
}
可以看出,内部的两个循环 j j j , k k k, 将反复读取矩阵 m a t r i x _ b [ N ] [ N ] matrix\_b[N][N] matrix_b[N][N] 的全部的 N × N N\times N N×N 个元素,以及反复读取矩阵 m a t r i x _ a [ N ] [ N ] matrix\_a[N][N] matrix_a[N][N] 的全部的第 i i i 行的 N N N 个元素,所产生的结果 r e s res res 将存入至结果矩阵 m a t r i x _ c [ N ] [ N ] matrix\_c[N][N] matrix_c[N][N] 的第 i i i 行。
考虑此时 Cache 的失效情况,抛开 Cache 大小无限制或者 Cache 的大小完全能装下这三个数组的最为理想的情况。考虑对三个数组访问时,导致 Cache 失效的情况,最坏的情况是,每次的访问都失效,这是会导致共 2 N 3 + N 2 2N^3 + N^2 2N3+N2 次的失效。为了减少失效,下面有两种方法能显著的减小 Cache 的失效率。
1 将矩阵转置
void mult() {
int i,j,k;
for(i=0;i<N;i++) {
for(j=0;j<N;j++)
matrix_c[j][i]=matrix_b[i][j];
}
for(int i=0;i<N;i++) {
for(int j=0;j<N;j++) {
double res=0.0;
for(int k=0;k<N;k++) {
res += (matrix_a[i][k]*matrix_c[j][k]);
}
matrix_c[i][j] = res;
}
}
}
毫无疑问的是,转置矩阵也是一个耗时的过程,但在转置处花费的时间和后面计算矩阵的乘积结果所节省的时间达到了很好的折中,下面是测试图:
(矩阵规模是 1025 × 1025 1025\times 1025 1025×1025,生成矩阵的方法是生成随机的 double 类型的浮点数 )
可以看到,转置操作再加上转置后的矩阵乘的开销远远小于直接矩阵乘的开销。
其中最主要的原因就是,转置后的矩阵乘法,最内部的循环 k k k ,都将反复读取矩阵 m a t r i x _ a [ N ] [ N ] matrix\_a[N][N] matrix_a[N][N] 的全部的第 i i i 行的 N N N 个元素,以及矩阵 m a t r i x _ b [ N ] [ N ] matrix\_b[N][N] matrix_b[N][N] 的全部的第 j j j 行的 N N N 个元素,而这样的读取方式,很好地利用了空间局部性,让对于程序员透明的 Cache 发挥了重要的作用。
2 分块处理矩阵乘法
分块是一种经典的利用 Cache 来提升程序性能的技术。分块算法不是对数组的整行或者整列进行访问的,而是把对大数组的访问分解成对子矩阵的访问。
为了保证正在访问的元素能在 Cache 中命中,把原程序内部改为仅仅对大小为 B × B B\times B B×B 的子数组进行计算,其中 B B B 称为分块因子,代码如下:
void multB() {
int jj,kk,i,j,k;
double r;
for(jj=0;jj<N;jj+=B)
for(kk=0;kk<N;kk+=B)
for(i=0;i<N;i++) //处理 BxB 大小的子矩阵
for(j=jj;j<Min((jj+B),(N));j++){
r=0.0;
for(k=kk;k<Min((kk+B),(N));k++)
r+=(matrix_a[i][k]*matrix_c[k][j]);
matrix_c[i][j]+=r;
}
}
这时再以开始的条件考虑失效次数,可以初步判断 Cache 的失效率会降低,测试结果如下:
3 将分块和转置结合
代码如下:
void mult_T_and_B() {
int jj,kk,i,j,k;
double r;
for(i=0;i<N;i++) {
for(j=0;j<N;j++)
matrix_c[j][i]=matrix_b[i][j];
}
for(jj=0;jj<N;jj+=B)
for(kk=0;kk<N;kk+=B)
for(i=0;i<N;i++)
for(j=jj;j<Min((jj+B),(N));j++){
r=0.0;
for(k=kk;k<Min((kk+B),(N));k++)
r+=(matrix_a[i][k]*matrix_c[j][k]);
matrix_c[i][j]+=r;
}
}
测试时间如下:
4 总结
程序的平均访存公式如下:
平
均
访
存
时
间
=
命
中
时
间
+
失
效
率
×
失
效
开
销
平均访存时间=命中时间+失效率\times 失效开销
平均访存时间=命中时间+失效率×失效开销
-
以上讨论的提升程序性能的方法的根本就是在降低失效率。
-
虽然Cache 对程序员而言是透明的,但深入理解 Cache 的基本原理,能在不经意间提高程序的性能。