CacheLab(附Excellent优化思路)
前言
因为这部分我感觉我学起来确实困难想不清楚,遂决定做个笔记整理一下。写着写着想到“干脆就发个博客和同学们交流一下吧”,因此这篇博客就产生了!没有放出所有的源代码,仅仅是提供一些思路上的梳理和帮助😀!
作者才疏学浅,若内容有误,还请多多批评指正!
基础概念
主存
考虑主存地址为n位的计算机系统,主存大小即2n字节。
以B = 2b作为一个块的大小来和cache进行数据传输。
此时任一地址可以表示为:
Block Number(n-b位) | Offset(b位) |
---|
举个例子,若主存(64位)中存放了两个int类型(4字节)的变量a,b,那么:
a:0x0000000000000000 -> 0x0000000000000003
b:0x0000000000000004 -> 0x0000000000000007
这里为了方便,我们把a,b十六进制的最后一位展开为二进制,其余位均为0,暂时忽略。
a:0000 -> 0011
b:0100 -> 0111
不难想到,为了正确的储存和传输a,b,这里的块的大小应该为4个字节(b=2),下同。
值得注意的是b的大小是可以自己定的,因为取数就是地址+偏移,怎么都能取到!也可以让b=3把a,b放在一个块里传输,但是这样就避开了我想讲的东西了!
此时,剩下的62位自然成为a,b的Block Number,a的Block Number为0,b的为1。
Cache
这时,我们引入一个Cache结构,包含S=2s个高速缓存组(set),每个组包含E个高速缓存行(line)。
特别地,这里我们考虑一个s=1,e=1的cache结构,包含2个组,每组一行。
我们希望达到两个效果:
1.主存中同组的那些块仅能映射到Cache中对应组的那些行中
2.相邻的块能被分到不同的组
因此,我们这样划分地址:
Tag(n-s-b位) | Set(s位) | Offset(b位) |
---|
引入int变量c:0x0000000000000008 -> 0x000000000000000b
最后四位:1000 -> 1011
我们利用a,b,c来观察这种划分有什么好处:
name | Tag(61位) | Set(1位) | Offset(2位) |
---|---|---|---|
a | 000…00 | 0 | 00 -> 11 |
b | 000…00 | 1 | 00 -> 11 |
c | 000…01 | 0 | 00 -> 11 |
可以看到,相邻的a,b不在同一组,而a,c在同一组,利用Tag来区分同一组内的不同块。
实际上,对于任意的一对(s,b)的地址划分都具备这样的性质。这样划分非常巧妙,我们不用引入任何新的变量来储存信息,只需要单纯地根据地址就可以完成信息的分组和编号。
而E代表cache内每一组能存放多少个block。主存中每两个block就有一个block属于第0组,这显然很多!属于同一组的block最多在cache中同时存在e个,否则就需要替换。
替换策略
策略有很多,这里实现一个简单但是有效的。
LRU策略的实现:
- 缓存的每一块都设置一个计数器,初始时均为0。
- 访问命中时,所有块的计数值与命中块的计数值进行比较,如果某块计数值小于命中块的计数值, 则该块的计数值加 1;如果该块的计数值大于命中块的计数值,则数值不变;最后将命中块的计数器清为0。
- 访问未命中,需要替换/装入时,则选择计数值最大的块被替换/装入,其计数器清为0,而其它的计数器则加1(除了初始装入之外,计数值是不会出现相等情况的,可以思考一下为什么)。
- (照抄的指导书)
Part1-Cache模拟器
实现一个Cache模拟器
这应该是一个较简单的部分,只要理解Cache的原理和构成就不难模拟。
结构
-S=2s个组
-每个组包含e列
-每一列能够储存valid,Tag和Block信息
方法
1.初始化
2.根据传入地址解析出必要信息,存取block
3.实现LRU替换策略
解析
此外,解析指令也非常重要,推荐使用getopt
来解析命令行参数,使用fscanf(reader, "%llx,%d", &address, &size)
来解析文件内指令!
Part2-Cache友好的矩阵转置
例子
给定Cache结构
s=5,E=1,b=5
,编写一个实现32x32矩阵转置的C语言程序,使得miss数尽可能小。
1.首先理解矩阵在内存中怎么储存
在b等于5的情况下,矩阵A[32][32]:
Set1 | Set2 | Set3 | Set4 |
---|---|---|---|
Block0 | Block1 | Block2 | Block3 |
A[0][0]-A[0][7] | A[0][8]-A[0][15] | A[0][16]-A[0][23] | A[0][24]-A[0][31] |
Set4 | Set5 | Set6 | Set7 |
---|---|---|---|
Block4 | Block5 | Block6 | Block7 |
A[1][0]-A[1][7] | A[1][8]-A[1][15] | A[1][16]-A[1][23] | A[1][24]-A[1][31] |
Set8 | Set9 | Set10 | Set11 |
---|---|---|---|
Block8 | Block9 | Block10 | Block11 |
A[2][0]-A[2][7] | A[2][8]-A[2][15] | A[2][16]-A[2][23] | A[2][24]-A[2][31] |
Set12 | Set13 | Set14 | Set15 |
---|---|---|---|
Block12 | Block13 | Block14 | Block15 |
A[3][0]-A[3][7] | A[3][8]-A[3][15] | A[3][16]-A[3][23] | A[3][24]-A[3][31] |
Set16 | Set17 | Set18 | Set19 |
---|---|---|---|
Block16 | Block17 | Block18 | Block19 |
A[4][0]-A[4][7] | A[4][8]-A[4][15] | A[4][16]-A[4][23] | A[4][24]-A[4][31] |
Set20 | Set21 | Set22 | Set23 |
---|---|---|---|
Block20 | Block21 | Block22 | Block23 |
A[5][0]-A[5][7] | A[5][8]-A[5][15] | A[5][16]-A[5][23] | A[5][24]-A[5][31] |
Set24 | Set25 | Set26 | Set27 |
---|---|---|---|
Block24 | Block25 | Block26 | Block27 |
A[6][0]-A[6][7] | A[6][8]-A[6][15] | A[6][16]-A[6][23] | A[6][24]-A[6][31] |
Set28 | Set29 | Set30 | Set31 |
---|---|---|---|
Block28 | Block29 | Block30 | Block31 |
A[7][0]-A[7][7] | A[7][8]-A[7][15] | A[7][16]-A[7][23] | A[7][24]-A[7][31] |
这里我只列出来了A[0][0] - A[7][31]这8*32个矩阵元素,之后还有3组这样的元素没有列出。s=5,cache中一共只有32个set,因此以后的3组元素的Set编号将会与以上相同,即Set0-31,因此,例如说,A[0][0]和A[8][0]两个数在Cache中将会因为相同的组编号而发生冲突(e=1). 此外,如果要访问B的元素B[i][j],因为题目中强调A和B的起始地址间隔保证了是Cache容量的整倍数,B[i][j]和A[i][j]也会发生冲突,即属于同一个组(Set)。
2.优化原理
下面给出一个简单分析:
实现一个32x32的矩阵转置至少要两步:
1.将所有数从矩阵A里读出来
2.将所有取出来的数写进矩阵B
读和写都记一次内存访问,一个块可以存8个数,要获取一个块至少要miss一次(miss或者miss+eviction)
因此,最少的miss次数也要有 32x32x2/8 = 256次
为了实现这个miss次数,我们必须保证对于每个块都只访问一次!换言之,我们优化的任务也就是尽量减少对同一个块的反复访问!
3.分块优化
考虑一个普通的矩阵转置函数:
//M=32 N=32
void trans(int M, int N, int A[N][M], int B[M][N])
{
int i, j;
for (i = 0; i < N; i++) {
for (j = 0; j < M; j++) {
B[j][i] = A[i][j];
}
}
}
这样的访问不具有良好的空间局部性。
为什么?就只看程序开始的一小部分:
i = 0: {
for (j = 0; j < 8; j++) //loop1
B[j][i] = A[i][j];
for (j = 8; j < 16; j++) //loop2
B[j][i] = A[i][j];
}
loop1中B[0-7][0]分别存放在8个块里,因此取这8个块分别访问,产生8个miss,紧接着loop2中的B[8-15][0]和B[0-7][0]组号相同,会把B[0-7][0]全部替换掉!loop1将这八个块都访问了,但是却都只利用了1/8!
不难想到一种分块方法,将32x32的矩阵分成16个8x8的子矩阵,把loop1访问的8个块充分利用了再替换成下8个块,从而减少miss数。下面实战一下!
16X16矩阵
1.题目分析
题目中给的Cache结构是s=4,E=1,b=5
,这和上面例子是不一样的!请注意。
同理,分析一下矩阵在内存中的排布。
Set0 | Set1 |
---|---|
Block0 | Block1 |
A[0][0]-A[0][7] | A[0][8]-A[0][15] |
Set2 | Set3 |
---|---|
Block2 | Block3 |
A[1][0]-A[1][7] | A[1][8]-A[1][15] |
Set4 | Set5 |
---|---|
Block4 | Block5 |
A[2][0]-A[2][7] | A[2][8]-A[2][15] |
Set6 | Set7 |
---|---|
Block6 | Block7 |
A[3][0]-A[3][7] | A[3][8]-A[3][15] |
Set8 | Set9 |
---|---|
Block8 | Block9 |
A[4][0]-A[4][7] | A[4][8]-A[4][15] |
Set10 | Set11 |
---|---|
Block10 | Block11 |
A[5][0]-A[5][7] | A[5][8]-A[5][15] |
Set12 | Set13 |
---|---|
Block12 | Block13 |
A[6][0]-A[6][7] | A[6][8]-A[6][15] |
Set14 | Set15 |
---|---|
Block14 | Block15 |
A[7][0]-A[7][7] | A[7][8]-A[7][15] |
同样的,我只列出来了A[0-7][0-15]的这部分,剩下一半的组编号与这一半完全相同。
对于16x16矩阵我们该怎么分块?32x32是8x8,那么16x16是不是该分成4x4?什么是我们选择分块策略的决定性因素?
如果你不知道这个问题的答案,那么我也没办法,因为我也不知道,但是我们可以从下面的做法中得到启发。
(如果有人研究出了公式请务必教教我,我感觉是可以推导的,但是我不会。。。)
2.解法
首先确定目标: 16x16x2/8 = 64次,外加额外的3次miss(教程有说明),我们优化的极限是67次。
我们选取8x8的分块策略。
下面展示4个8x8块中的一块:i:0-7;j:0-7 。
Set(所在组) | A[i] | [j] |
---|---|---|
0 | 0 | 0-7 |
2 | 1 | 0-7 |
4 | 2 | 0-7 |
6 | 3 | 0-7 |
8 | 4 | 0-7 |
10 | 5 | 0-7 |
12 | 6 | 0-7 |
14 | 7 | 0-7 |
对这一块进行充分利用:
for (k = 0; k < 8; k++)//8x8的分块,k代表一个块的开始位置
{
//取A[k][0-7]会占用Cache的第 2k-2 组
tmp0 = A[k][0];
tmp1 = A[k][1];
tmp2 = A[k][2];
tmp3 = A[k][3];
tmp4 = A[k][4];
tmp5 = A[k][5];
tmp6 = A[k][6];
tmp7 = A[k][7];
//取B[0-7][k]占用Cache全部偶数组
B[0][k] = tmp0;
B[1][k] = tmp1;
B[2][k] = tmp2;
B[3][k] = tmp3;
B[4][k] = tmp4;
B[5][k] = tmp5;
B[6][k] = tmp6;
B[7][k] = tmp7;
}
分析:对于每一个k,读取A造成一次miss。k=0时,读取B造成8次miss,k>0时,由于读取A会挤占掉一个B的block,因此每个k会导致B重读一次,造成1次miss。
如果不算A,B相互替换而造成的miss数,A,B的block利用率均为100%,而相互排挤导致了7次额外的miss。这个块一共产生了8+7+8=23次miss。
那我们这样优化的结果是不是 23*4+3 = 95次呢?
还真不是,考虑以下情景:
for (k = 8; k < 15; k++)//8x8的分块,k代表一个块的开始位置
{
//取A[k][0-7]会占用Cache的第 2k-2 组
tmp0 = A[k][0];
tmp1 = A[k][1];
tmp2 = A[k][2];
tmp3 = A[k][3];
tmp4 = A[k][4];
tmp5 = A[k][5];
tmp6 = A[k][6];
tmp7 = A[k][7];
//取B[0-7][k]占用Cache全部奇数组
B[0][k] = tmp0;
B[1][k] = tmp1;
B[2][k] = tmp2;
B[3][k] = tmp3;
B[4][k] = tmp4;
B[5][k] = tmp5;
B[6][k] = tmp6;
B[7][k] = tmp7;
}
我们发现,这一块居然没有冲突!这一块只产生16个miss。
事实上,只有对角线上的块才会产生冲突,而别的块都没有这样的冲突!
因此,最后的miss数是: 23*2 + 16*2 +3 = 81!
这不就过了吗!
3.Excellent优化思路
聪明如你一定已经知道怎么优化了!
不知道的话还是要自己多思考一下哦~
没错,对角线上的块会产生14次额外的miss,这是我们不能忍受的。
怎么才能避免A和B的冲突?一个解决方案是开64个临时变量,将A的数据全部读取暂存,再全部放入B中。
很显然,上面只是我说着玩的,因为限制了只能有12个临时变量,那该怎么办呢?
没错,用B来暂存一下。寄存器有限,但是B我们可以随意更改呀~
核心思想是先暂存在B里,等A的下一行的元素访问完了再转置。
愿意写的自己写写吧,可以达到理论最优67个。
(我写的有点丑。。。完全可以写得更好!)
代码参考:
if (i == j)
{
k = i;
tmp0 = A[k][j];
tmp1 = A[k][j + 1];
tmp2 = A[k][j + 2];
tmp3 = A[k][j + 3];
tmp4 = A[k][j + 4];
tmp5 = A[k][j + 5];
tmp6 = A[k][j + 6];
tmp7 = A[k][j + 7];
B[k][j] = tmp0;
B[k][j + 1] = tmp1;
B[k][j + 2] = tmp2;
B[k][j + 3] = tmp3;
B[k][j + 4] = tmp4;
B[k][j + 5] = tmp5;
B[k][j + 6] = tmp6;
B[k][j + 7] = tmp7;
tmp0 = A[k + 1][j];
tmp1 = A[k + 1][j + 1];
tmp2 = A[k + 1][j + 2];
tmp3 = A[k + 1][j + 3];
tmp4 = A[k + 1][j + 4];
tmp5 = A[k + 1][j + 5];
tmp6 = A[k + 1][j + 6];
tmp7 = A[k + 1][j + 7];
B[k + 1][j] = B[k][j + 1];
B[k][j + 1] = tmp0;
B[k + 1][j + 1] = tmp1;
B[k + 1][j + 2] = tmp2;
B[k + 1][j + 3] = tmp3;
B[k + 1][j + 4] = tmp4;
B[k + 1][j + 5] = tmp5;
B[k + 1][j + 6] = tmp6;
B[k + 1][j + 7] = tmp7;
tmp0 = A[k + 2][j];
tmp1 = A[k + 2][j + 1];
tmp2 = A[k + 2][j + 2];
tmp3 = A[k + 2][j + 3];
tmp4 = A[k + 2][j + 4];
tmp5 = A[k + 2][j + 5];
tmp6 = A[k + 2][j + 6];
tmp7 = A[k + 2][j + 7];
B[k + 2][j] = B[k][j + 2];
B[k + 2][j + 1] = B[k + 1][j + 2];
B[k][j + 2] = tmp0;
B[k + 1][j + 2] = tmp1;
B[k + 2][j + 2] = tmp2;
B[k + 2][j + 3] = tmp3;
B[k + 2][j + 4] = tmp4;
B[k + 2][j + 5] = tmp5;
B[k + 2][j + 6] = tmp6;
B[k + 2][j + 7] = tmp7;
tmp0 = A[k + 3][j];
tmp1 = A[k + 3][j + 1];
tmp2 = A[k + 3][j + 2];
tmp3 = A[k + 3][j + 3];
tmp4 = A[k + 3][j + 4];
tmp5 = A[k + 3][j + 5];
tmp6 = A[k + 3][j + 6];
tmp7 = A[k + 3][j + 7];
B[k + 3][j] = B[k][j + 3];
B[k + 3][j + 1] = B[k + 1][j + 3];
B[k + 3][j + 2] = B[k + 2][j + 3];
B[k][j + 3] = tmp0;
B[k + 1][j + 3] = tmp1;
B[k + 2][j + 3] = tmp2;
B[k + 3][j + 3] = tmp3;
B[k + 3][j + 4] = tmp4;
B[k + 3][j + 5] = tmp5;
B[k + 3][j + 6] = tmp6;
B[k + 3][j + 7] = tmp7;
tmp0 = A[k + 4][j];
tmp1 = A[k + 4][j + 1];
tmp2 = A[k + 4][j + 2];
tmp3 = A[k + 4][j + 3];
tmp4 = A[k + 4][j + 4];
tmp5 = A[k + 4][j + 5];
tmp6 = A[k + 4][j + 6];
tmp7 = A[k + 4][j + 7];
B[k + 4][j] = B[k][j + 4];
B[k + 4][j + 1] = B[k + 1][j + 4];
B[k + 4][j + 2] = B[k + 2][j + 4];
B[k + 4][j + 3] = B[k + 3][j + 4];
B[k][j + 4] = tmp0;
B[k + 1][j + 4] = tmp1;
B[k + 2][j + 4] = tmp2;
B[k + 3][j + 4] = tmp3;
B[k + 4][j + 4] = tmp4;
B[k + 4][j + 5] = tmp5;
B[k + 4][j + 6] = tmp6;
B[k + 4][j + 7] = tmp7;
tmp0 = A[k + 5][j];
tmp1 = A[k + 5][j + 1];
tmp2 = A[k + 5][j + 2];
tmp3 = A[k + 5][j + 3];
tmp4 = A[k + 5][j + 4];
tmp5 = A[k + 5][j + 5];
tmp6 = A[k + 5][j + 6];
tmp7 = A[k + 5][j + 7];
B[k + 5][j] = B[k][j + 5];
B[k + 5][j + 1] = B[k + 1][j + 5];
B[k + 5][j + 2] = B[k + 2][j + 5];
B[k + 5][j + 3] = B[k + 3][j + 5];
B[k + 5][j + 4] = B[k + 4][j + 5];
B[k][j + 5] = tmp0;
B[k + 1][j + 5] = tmp1;
B[k + 2][j + 5] = tmp2;
B[k + 3][j + 5] = tmp3;
B[k + 4][j + 5] = tmp4;
B[k + 5][j + 5] = tmp5;
B[k + 5][j + 6] = tmp6;
B[k + 5][j + 7] = tmp7;
tmp0 = A[k + 6][j];
tmp1 = A[k + 6][j + 1];
tmp2 = A[k + 6][j + 2];
tmp3 = A[k + 6][j + 3];
tmp4 = A[k + 6][j + 4];
tmp5 = A[k + 6][j + 5];
tmp6 = A[k + 6][j + 6];
tmp7 = A[k + 6][j + 7];
B[k + 6][j] = B[k][j + 6];
B[k + 6][j + 1] = B[k + 1][j + 6];
B[k + 6][j + 2] = B[k + 2][j + 6];
B[k + 6][j + 3] = B[k + 3][j + 6];
B[k + 6][j + 4] = B[k + 4][j + 6];
B[k + 6][j + 5] = B[k + 5][j + 6];
B[k][j + 6] = tmp0;
B[k + 1][j + 6] = tmp1;
B[k + 2][j + 6] = tmp2;
B[k + 3][j + 6] = tmp3;
B[k + 4][j + 6] = tmp4;
B[k + 5][j + 6] = tmp5;
B[k + 6][j + 6] = tmp6;
B[k + 6][j + 7] = tmp7;
tmp0 = A[k + 7][j];
tmp1 = A[k + 7][j + 1];
tmp2 = A[k + 7][j + 2];
tmp3 = A[k + 7][j + 3];
tmp4 = A[k + 7][j + 4];
tmp5 = A[k + 7][j + 5];
tmp6 = A[k + 7][j + 6];
tmp7 = A[k + 7][j + 7];
B[k + 7][j] = B[k][j + 7];
B[k + 7][j + 1] = B[k + 1][j + 7];
B[k + 7][j + 2] = B[k + 2][j + 7];
B[k + 7][j + 3] = B[k + 3][j + 7];
B[k + 7][j + 4] = B[k + 4][j + 7];
B[k + 7][j + 5] = B[k + 5][j + 7];
B[k + 7][j + 6] = B[k + 6][j + 7];
B[k][j + 7] = tmp0;
B[k + 1][j + 7] = tmp1;
B[k + 2][j + 7] = tmp2;
B[k + 3][j + 7] = tmp3;
B[k + 4][j + 7] = tmp4;
B[k + 5][j + 7] = tmp5;
B[k + 6][j + 7] = tmp6;
B[k + 7][j + 7] = tmp7;
}
32X32矩阵
1.解法
都写到这里了,我也懒得再画一遍内存图了,大家自己画一画就好了。基本的思想和之前还是一模一样的!
唯一不同于16x16的是,我们发现如果8x8分块的话,不仅A,B之间有冲突,甚至就连A,B内部上四行和下四行都有冲突!
难道要用4x4?不行,4x4引入新问题:一个block有8个数,你都只用4个,那miss率不得上天去了!
因此我们想要结合两者的优点:使用8x8的分块,但是在内部以4x4为单位操作,先完全操作上半4x8,再操作下半。
提示:在考虑能不能AC而不是Excellent时,应先不要考虑A1A2 和B1B2的冲突,而是着眼于A1A2 和A3A4的冲突
因为只有对角线块会产生A,B之间的冲突,而每个块A1A2 和 A3A4都是冲突的!
如果把一个8x8的块分为4份:
A1 | A2 |
---|---|
A3 | A4 |
块内的操作可以分成以下步骤:
- 步骤一
- 将A1, A2数据复制给B1, B2
- 步骤二
- 将A3的数据传给B2,同时,B2的数据传给B3(这里注意如何利用8个临时变量来保证不会产生额外的miss,这个比较tricky)
- 步骤三
- 将A4复制给B4
代码就自己写啦~
2.miss分析
非对角线块只有上下冲突,我们已经解决了,因此只有16个miss!还是很牛的!
但是还有4大个对角线块,我们没有做任何优化,因此每个对角线块都会额外产生19个miss!恁多!
幸运的是,即便如此,总miss仍然小于400!过了!!那要不然就不优化了吧。
3.Excellent优化思路
不行,就是要优化!
同样的,优化的思路就是去尝试解决对角线块的冲突问题。
我的想法很简单,块内有3个操作步骤,每一步都可以当做独立的一部分进行优化(就像16x16矩阵中的那样)
但是我没办法优化优化到理论最优解,作业太多,遂放弃。
具体就不细讲了,感兴趣的自己可以研究一下。
补:
我找到一篇文章,里面的思路可以把这道题优化到理论最优解259次(只能膜了,非常巧妙)
https://zhuanlan.zhihu.com/p/387662272