前言
上次做计算机系统的实验,做到一个高速缓存的实验,不过实验内容比较简单,就是验证一下不同的数据存取方式对缓存命中率的影响(通过运行时间体现)
因为我上课全程摸鱼,所以今天来回顾一下高速缓存这个内容,加深映像(
高速缓存介绍
什么是高速缓存
高速缓存是介于cpu和内存之间的一级存储器。访问cpu高速缓存的速度要快于访问内存,因此高速缓存常常用来加速cpu的运算。
你可以在任何一本介绍计算机基础的课本上找到类似的图(图片源 百度图片)来描述计算机的各种存储器:
高速缓存基本原理
cpu的速度远远快于内存,因此如果cpu只是从内存中读取数据,那么会花费较多的时间在等待数据上,我们希望有一种方法解决【从内存中读数据慢】的问题,于是有了高速缓存。
对于数据的读取基于两个猜想,假设我们读取内存地址x处的数据,那么有
- 内存地址x处的数据在短时间内容易被再次访问
- 内存地址x周围的数据在短时间内容易被再次访问
以上两个猜想很容易得到证实,(这也是程序的局部性原理的一部分),比如我们会经常编写这样的代码
// 猜想1: 频繁地访问指针a指向的地址存储的变量
int* a = new int;
*a = 123;
for(int i=0; i<n; i++)
{
*a += i;
}
// 猜想2: 矩阵运算的时候 频繁访问相邻的变量 a[i+1]
int a[10];
int b[10];
for(int i=0; i<9; i++)
{
a[i] += b[i+1];
a[i+1] += b[i];
}
频繁访问内存意味着速度慢。高速缓存的引入解决了这个问题
hit
我们在访问一个内存地址的时候,先查看高速缓存里面有无该地址的数据。如果有就直接从高速缓存中读取数据,这个行为我们叫做hit,即缓存命中
miss
如果高速缓存中没有数据,我们需要从内存中读取数据,这个行为叫做miss。值得注意的是,我们一次读取不是读取一个内存单元的数据,而是读取一个【cache行】的数据,这意味着目标地址及其附近区域的一些数据会被读到高速缓存中,如果下次访问邻近的数据,就不会miss。(cache行一般是64字节)
图解
下面通过一张图解释高速缓存的基本原理,这个原理告诉我们,对内存中的数据的访问,数据们相距的越近,数据越集中,存取速度越快。
更加详细
在《深入理解计算机系统》书中,给出了相当详细的对于高速缓存的解释,我们可以来一探究竟
简单的来说,就是取数据的时候,先查看数据地址,然后做一次硬件哈希映射,查找缓存里是否命中,然后按照情况做对应的操作。
简单实验验证
我们模拟简单的矩阵乘法,编写两个子程序,计算res=a*b
的矩阵乘法,我们使用一维数组来模拟n*n
大小的矩阵
其中1是正常计算,即按行访问a矩阵的元素,然后按列访问b矩阵的元素,最后对应位置相乘,写入res矩阵。
void matrix_multiply_1(int* m1, int* m2, int* res, int n)
{
for(int i=0; i<n; i++)
for(int j=0; j<n; j++)
for(int k=0; k<n; k++)
res[i*n+j] += m1[i*n+k]*m2[i*k+j];
}
我们将1中的循环顺序替换一下,由ijk
变为ikj
,性能大提升!
void matrix_multiply_2(int* m1, int* m2, int* res, int n)
{
for(int i=0; i<n; i++)
for(int k=0; k<n; k++)
for(int j=0; j<n; j++)
res[i*n+j] += m1[i*n+k]*m2[i*k+j];
}
可以看到在3000x3000的规模下 1用时174s,而2用时仅仅115s
你可能觉得总的循环次数不变,为何速度相差这么大?
如果k在最内层,变化频次最高,这意味着大量相隔很远的数据(因为以一维数组形式模拟矩阵在内存中的存放)将会被频繁读入cache中,而cache行存储的是邻近的几个数据。
后一次访问的地址,不在前一次访问带来的cache存储的范围内,当然miss率高
下面两幅图对比了两种存取方式,黄色方块是我们矩阵乘法访问的元素,红色方块是访问目标元素时带来的cache行存储。
方式1:每一列都要miss
方式2:每读取完一个cache行,仅仅在首元素发生miss,其他元素均随着首元素的读入而进入cache
代码
#include <bits/stdc++.h>
using namespace std;
void matrix_multiply_1(int* m1, int* m2, int* res, int n)
{
for(int i=0; i<n; i++)
for(int j=0; j<n; j++)
for(int k=0; k<n; k++)
res[i*n+j] += m1[i*n+k]*m2[i*k+j];
}
void matrix_multiply_2(int* m1, int* m2, int* res, int n)
{
for(int i=0; i<n; i++)
for(int k=0; k<n; k++)
for(int j=0; j<n; j++)
res[i*n+j] += m1[i*n+k]*m2[i*k+j];
}
int main()
{
double t1, t2;
clock_t st, ed;
int n;
cin>>n;
int* m1 = new int[n*n];
int* m2 = new int[n*n];
int* res = new int[n*n];
for(int i=0; i<n; i++)
for(int j=0; j<n; j++)
m1[i*n+j]=rand()%1000;
for(int i=0; i<n; i++)
for(int j=0; j<n; j++)
m2[i*n+j]=rand()%1000;
memset(res, 0, sizeof(res));
st = clock();
matrix_multiply_1(m1, m2, res, n);
ed = clock();
t1 = (double)(ed-st) / CLOCKS_PER_SEC;
memset(res, 0, sizeof(res));
st = clock();
matrix_multiply_2(m1, m2, res, n);
ed = clock();
t2 = (double)(ed-st) / CLOCKS_PER_SEC;
cout<<t1<<endl<<t2<<endl;
return 0;
}