最近两天有幸接受Intel公司的培训。然后培训的时候讲到cache命中的问题。了解到cpu在从memory中load数据的时候其实是将memory中的地址连续的一段数据都load到cache中的,而这时基于这样一个设想:即当要用到的数据附近的数据就是下一次或者下面几次将要进行的操作的数据。。所以将地址连续的一段数据load到cache中,这样将来操作的时候就可以直接从cache而不是内存中取数据,增加cache命中率,减少数据传输的时间。进而提高程序性能。
当时还讲到一个具体的例子,,最典型的就是矩阵加法。。因为矩阵加法必须有二重循环,形式如下:
for(size i = 0; i < HEIGHT; ++i){
for(size j = 0; j < WIDTH; ++j){
C[i][j] = A[i][j] + B[i][j];
}
}
这是一个不好的实现,因为内层循环变量时j递增时,我们可以具体看看循环里面的操作:
C[j][i]的访问是跳跃式的前进的(stride),比如j = 1时,我们将会访问C[1][i],这样,C[1][i]附近的数据将会被load到cache,但是j递增之后。我们下一个操作访问的数据时C[2][i],它和C[1][i]地址差距为i,这样C[2][i]不一定在缓存中。还得重新到memory中load。减缓了速度。。
相反,我们再看看另外一种实现:
for(size i = 0; i < HEIGHT; ++i){
for(size j = 0; j < WIDTH; ++j){
C[i][j] = A[i][j] + B[i][j];
}
}
针对此种实现,对于每一次递增的j,内存地址的增加都是挨着的,所以内存访问效率更高,当然程序速度更快。。
以下是本人基于上述两种方法对矩阵加法的实现,并对比给出执行时间的差距:
1.跳跃式访问:
#include <stdio.h>
#include<time.h>
#include<stdlib.h>
#define WIDTH 20000
#define HEIGHT 20000
#define MAX 100
template<typename size>
size** matAdd(size** A, size** B){
/*
size height = sizeof(A)/sizeof(A[0]);
printf("sizeof(A) is :%d\n",sizeof(A));
printf("sizeof(A[0]) is:%d\n",sizeof(A[0]));
printf("height is: %d\n", height);
size width = sizeof(A[0])/sizeof(A[0][0]);
printf("width is :%d\n", width);
*/
size** C = new size*[HEIGHT];
for(size i = 0; i < HEIGHT; ++i){
C[i] = new size[WIDTH];
}
for(size i = 0; i < HEIGHT; ++i){
for(size j = 0; j < WIDTH; ++j){
C[i][j] = A[i][j] + B[i][j];
}
}
return C;
}
int main(){
typedef int size;
size** A = new size*[HEIGHT];
for(size i = 0; i < HEIGHT; ++i){
A[i] = new size[WIDTH];
}
srand((int)time(0));
size** B = new size*[HEIGHT];
for(size i = 0; i < HEIGHT; ++i){
B[i] = new size[WIDTH];
}
for(size i = 0; i < HEIGHT; ++i){
for(size j = 0; j < WIDTH; ++j){
A[i][j] = rand() % MAX;
B[i][j] = rand() % MAX;
}
}
size** C = matAdd(A, B);
}
执行时间如下所示:
time ./matAdd
real 0m57.507s
user 0m54.718s
sys 0m2.781s
相对比下:连续访问内存实现方式如下:
#include <stdio.h>
#include<time.h>
#include<stdlib.h>
#define WIDTH 20000
#define HEIGHT 20000
#define MAX 100
template<typename size>
size** matAdd(size** A, size** B){
/*
size height = sizeof(A)/sizeof(A[0]);
printf("sizeof(A) is :%d\n",sizeof(A));
printf("sizeof(A[0]) is:%d\n",sizeof(A[0]));
printf("height is: %d\n", height);
size width = sizeof(A[0])/sizeof(A[0][0]);
printf("width is :%d\n", width);
*/
size** C = new size*[HEIGHT];
for(size i = 0; i < HEIGHT; ++i){
C[i] = new size[WIDTH];
}
for(size i = 0; i < HEIGHT; ++i){
for(size j = 0; j < WIDTH; ++j){
C[i][j] = A[i][j] + B[i][j];
}
}
return C;
}
int main(){
typedef int size;
size** A = new size*[HEIGHT];
for(size i = 0; i < HEIGHT; ++i){
A[i] = new size[WIDTH];
}
srand((int)time(0));
size** B = new size*[HEIGHT];
for(size i = 0; i < HEIGHT; ++i){
B[i] = new size[WIDTH];
}
for(size i = 0; i < HEIGHT; ++i){
for(size j = 0; j < WIDTH; ++j){
A[i][j] = rand() % MAX;
B[i][j] = rand() % MAX;
}
}
size** C = matAdd(A, B);
}
执行时间如下所示:
time ./matAdd
real 0m22.881s
user 0m20.197s
sys 0m2.680s
由此可见,性能相差一倍有余。这也更加体现出连续访问内存的重要性。。