《CS:App》实验 Cache Lab 解析
准备
前置介绍、准备
1.《CS:App》官网:http://csapp.cs.cmu.edu/3e/labs.html
WriteUp中的pdf需要好好看,里面有详细的规则和建议。
2.Blocking 分块技术http://csapp.cs.cmu.edu/2e/waside/waside-blocking.pdf
这是PartB中需要用到的技术。
3.Valgrind的导出文件
PartA
使用Valgrind
的导出文件作为模拟器的输入。其内容为
#yi.trace
I xxxx
L 10,1
M 20,1
L 22,1
S 18,1
L 110,1
L 210,1
M 12,1
I => Instruction L => Load S => Store M => Modify
operation | address | size |
---|---|---|
Load(data) | 10 | 1 |
Modify(data) | 20 | 1 |
Load(data) | 22 | 1 |
Store(data) | 18 | 1 |
… | … | … |
其中L
与S
只访问一次Cache;M
会先L
再S
,访问两次Cahce,同时其第二次访问必命中。
还有I
指令,这里没有列出。它表示指令加载,这里我们不做处理,过滤即可。因为这里的Cache我们只存数据,不存指令。
实验总述
该实验分为两部分
Part A: Writing a Cache Simulator
实现一个Cache模拟器。
Part B: Optimizing Matrix Transpose
实现并优化一个矩阵转置函数。
Part A: Writing a Cache Simulator
任务描述
1.任务
在csim.c
中使用LRU算法
完成Cache模拟器的代码编写,并与核验程序csim-ref
的执行结果(hits misses evictions数)相同。该模拟器以Valgrind
的导出文件作为输入。
linux> ./csim-ref -v -s 4 -E 1 -b 4 -t traces/yi.trace
L 10,1 miss
M 20,1 miss hit
L 22,1 hit
S 18,1 hit
L 110,1 miss eviction
L 210,1 miss eviction
M 12,1 miss eviction hit
hits:4 misses:5 evictions:3
2.规则
- 代码内不能出现Warning,否则编译失败
- 将Hits Misses Evictions 传给printSummary()来显示
- 因为需要指定s E b,所以需要使用malloc动态分配
- 本实验假定内存访问已正确对齐,像
size=1/4/8
都可以存在一行Cache中,也同时读取这么多,所以我们可以忽略trace文件中的size。
分析
分析好需求+明确LRU、I/L/S/M指令
的定义,应该就能写出来了,难度像学生管理系统^^。
实现
因为代码较长,CSDN还不能折叠。就放在网盘了。
PartA PartB C文件-微云
PartA PartB C文件-CSDN免积分
Part B: Optimizing Matrix Transpose
任务描述
1.任务
编写一个Cache (s=5,E=1,b=5)
友好的转置函数。在trans.c
文件中补全transpose_submit()
函数,以尽可能少的miss数完成矩阵A->矩阵B的转置,即A[m][n]->B[n][m]。
trans.c
中已提供一个简单的转置函数,可是它有很多的miss。
2.规则
- 不能有Warning,否则无法通过编译
- 每一个转置函数最多使用
12个int型局部变量
,所有的函数使用的局部变量总数也不能超过12,不能再声明数组、使用malloc
- 函数不能递归
- 矩阵数组A不能变动,B可以随意改变。
3.评定标准
共需要优化3种大小的数组,分别为32×32
64×64
61×67
,评定标准为
分析
这里使用的Cache是(s=5,E=1,b=5)
,32组×32Bytes;其一组可以存8个int变量,这意味着“每8行矩阵,其对应的Cache就会开始循环”。
装完A[7]行时已经把Cache已满。
显然整个Cache都装不下矩阵A或B,如果不做处理的话,那就是这种默认的情况。misses数达到了1183个。
在默认情况下,miss过程:
- 按行读
A[0][0]
,miss(cold miss),Cache加载A[0][0]~A[0][7]
到Cache块0; - 按列写
B[0][0]
,miss(conflict miss),Cache加载B[0][0]~B[0][7]
到Cache块0(冲突,替换掉A[0][0]~A[0][7]); - 按行读
A[0][1]
,miss(conflict miss,上次B替换掉了A的内容),Cache加载A[0][0]~A[0][7]
到Cache块0; - 按列写
B[1][0]
,miss(cold miss),Cache加载B[1][0]~B[1][7]
到Cache块4; - 按行读
A[0][2]
,hit,在“3.”中Cache已加载A[0][0]~A[0][7]
到Cache块0; - 按列写
B[2][0]
,miss(cold miss),Cache加载B[2][0]~B[2][7]
到Cache块8; - …
- 按行读
A[0][8]
,miss(cold miss),Cache加载A[0][8]~A[0][15]
到Cache块1; - 按列写
B[8][0]
,miss(conflict miss),Cache加载B[8][0]~B[8][7]
到Cache块0(冲突,替换掉A[0][0]~A[0][7]); - …
从上述过程中可以知道,默认情况下miss主要由:
- 开始新一行的冷不命中,A每一行有4次;B每列每次都会不命中,一列有32次,共
4×32+32×32=1152
次miss - 对角线上的元素反复冲突,像
"1." "2."和"3."过程
。共31次 (原因可看下面分析,这里有个映像即可)
所以共1152+31=1183
次miss,与我们跑出来的结果一致
32×32
分析
我们先来解决 默认情况下,矩阵B的Cache全部冲突的的问题。
造成该问题的主要原因
为 “Cache最多能装矩阵B的8行,第9行开始循环使用Cache块号。造成B加载的行B[0]~ B[7]每行只用了一次B[0][0]、B[1][0]…就被后续读取的行B[8]~B[15]覆盖掉,存在很多浪费和miss。”
次要原因
为 对角线上的元素反复冲突,像"1." "2."和"3."过程。 还有B[1][1] B[2][2] ...
这些与A进行Cache的争夺,A需要加载两次,非对角线块却只需要加载一次。过程为:
- 读
A[1][0]
,加载A[1][0]~A[1][7]
- 写
B[0][1]
,加载B[0][0]~B[0][7]
- 读
A[1][1]
,Cache已加载,命中了。 - 写
B[1][1]
,加载B[1][0]~B[1][7]
,替换掉了"1."
中A的Cache块 - 读
A[1][2]
,再次加载A[1][0]~A[1][7]
- …
核心点就是对角线上,A的上一次加载会被B替换,A需要再加载一次。
解决
解决主要原因
使用Blocking分块技术,将整个矩阵分成小矩阵,使得Cache能够完全处理一个小矩阵,充分利用Cache。
这里使用8×8
的分块大小,因为
1. Cache最多只能装矩阵8行,超过8行开始循环使用Cache块号。所以我们分块要将大小控制在8行及以内。
2. Cache一组能装8个int值。
两者结合,8×8
分块呼之欲出。分完后的结构是:
A和B是一样的分块结构。此时矩阵B的Cache不会发生冲突。如A的黄色分块对应到B的黄色分块,A读黄色分块的1~ 29,对应对B写黄色分块的0~28块,不会使用相同的Cache块,故不会冲突。A的黄色方块没有读完不会读下一个蓝色方块,其对应的B黄色方块没有转置完A的黄色方块也不会读下一个方块蓝。
这样就避免了“矩阵B[8]~ B[15]
替换B[0]~ B[7]
行,B[0]~ B[7]
却只用了一次就丢弃” 造成的浪费和分块内部的冲突。
代码(建议先看一看,了解如何遍历,如何读取,建立起一个模型)
更改trans.c文件后,需要重新编译整个项目 make clean & make
if(M==32 && N==32)
{
for(i=0;i<N;i+=8)
for(j=0;j<M;j+=8)
for(k=i;k<i+8;k++)
for(m=j;m<j+8;m++)
B[m][k]=A[k][m];
}
还不错,从1183->343,但还不够,需要miss<300才能满分,所以继续优化。
解决次要原因
核心原因是对角线上,A的上一次加载会被B替换,A需要再加载一次。 那我们可以使A只读一次在缓冲中,B加载替换了也没关系,我们从缓冲中读A。
看看题目要求,我们可以利用那12个int型变量,用其中8个变量存A分块的一行(8个)。这样就避免了二次加载A。
代码
if(M==32 && N==32)
for(i=0;i<N;i+=8)
for(j=0;j<M;j+=8)
for(k=i;k<i+8;k++)
{
v1=A[k][j+0];
v2=A[k][j+1];
v3=A[k][j+2];
v4=A[k][j+3];
v5=A[k][j+4];
v6=A[k][j+5];
v7=A[k][j+6];
v8=A[k][j+7];
B[j+0][k]=v1;
B[j+1][k]=v2;
B[j+2][k]=v3;
B[j+3][k]=v4;
B[j+4][k]=v5;
B[j+5][k]=v6;
B[j+6][k]=v7;
B[j+7][k]=v8;
}
此时已达到满分,但还能继续优化。
理论最小miss值优化
目标miss值
:
- 矩阵A的
8×8
分块的每一行产生一次miss,共32×4=128
次 - 矩阵B的
8×8
分块每一行产生一次miss,共32×4=128
次
共256次。但此时的优化结果为287,相差31,很显然只能是B的对角线还存在miss。
过程
:
- 读8×8分块
A[0][0]~A[0][7]
,加载A[0][0]~A[0][7]
- 写8×8分块
B[0][0]~B[7][0]
,加载B[0][0]~B[0][7] ... B[7][0]~[7][7]
- 读8×8分块
A[1][0]~A[1][7]
,加载A[1][0]~A[1][7]
,替换掉B的部分 - 写8×8分块
B[0][1]~B[7][1]
,只加载B[1][0]~B[1][7]
,其他的行上次已加载,替换掉"3."
中A的部分
关键原因为对角线上,B的上一次加载会被A替换,B需要再加载一次。
解决
:
两种思路:
- 在A访问第二行之前(即第一行),B不访问第二行
- 在A访问第二行之后,B不访问第二行
第一种思路容易想到,之前我们是“读A 8×8分块的第一行,立刻转置到B
8×8
分块的第一列”,此时B必然会访问到第二行。
我们可以先将A的第二行读取了,存放在B中(因为A不可改动)第一行(因为B是按列访问,它的第一行暂时不会会使用),B在这个过程中只触发一次冷不命中,并且后续还能继续使用而不会造成miss。
只有位于对角线上的8×8
分块中会造成B的此类不命中,未在对角线上的8×8
分块B是不会造成此类不命中的。
所以我们只对对角线上的分块进行手动处理
,其他的分块我们可以直接用B[m][k]=A[k][m]
而不需要用第二次优化的临时变量了,因为A中的对角线元素命中同样会被手动处理
解决。
代码
if(M==32 && N==32)
{
for(i=0;i<M;i+=8)
for(j=0;j<M;j+=8)
if(i==j)
{
m=i;
v1=A[m][j];v2=A[m][j+1];v3=A[m][j+2];v4=A[m][j+3];
v5=A[m][j+4];v6=A[m][j+5];v7=A[m][j+6];v8=A[m][j+7];
B[m][j]=v1;B[m][j+1]=v2;B[m][j+2]=v3;B[m][j+3]=v4;
B[m][j+4]=v5;B[m][j+5]=v6;B[m][j+6]=v7;B[m][j+7]=v8;
v1=A[m+1][j];v2=A[m+1][j+1];v3=A[m+1][j+2];v4=A[m+1][j+3];
v5=A[m+1][j+4];v6=A[m+1][j+5];v7=A[m+1][j+6];v8=A[m+1][j+7];
B[m+1][j]=B[m][j+1];B[m][j+1]=v1;
B[m+1][j+1]=v2;B[m+1][j+2]=v3;B[m+1][j+3]=v4;
B[m+1][j+4]=v5;B[m+1][j+5]=v6;B[m+1][j+6]=v7;B[m+1][j+7]=v8;
v1=A[m+2][j];v2=A[m+2][j+1];v3=A[m+2][j+2];v4=A[m+2][j+3];
v5=A[m+2][j+4];v6=A[m+2][j+5];v7=A[m+2][j+6];v8=A[m+2][j+7];
B[m+2][j]=B[m][j+2];B[m+2][j+1]=B[m+1][j+2];
B[m][j+2]=v1;B[m+1][j+2]=v2;B[m+2][j+2]=v3;
B[m+2][j+3]=v4;B[m+2][j+4]=v5;B[m+2][j+5]=v6;B[m+2][j+6]=v7;B[m+2][j+7]=v8;
v1=A[m+3][j];v2=A[m+3][j+1];v3=A[m+3][j+2];v4=A[m+3][j+3];
v5=A[m+3][j+4];v6=A[m+3][j+5];v7=A[m+3][j+6];v8=A[m+3][j+7];
B[m+3][j]=B[m][j+3];B[m+3][j+1]=B[m+1][j+3];B[m+3][j+2]=B[m+2][j+3];
B[m][j+3]=v1;B[m+1][j+3]=v2;B[m+2][j+3]=v3;B[m+3][j+3]=v4;
B[m+3][j+4]=v5;B[m+3][j+5]=v6;B[m+3][j+6]=v7;B[m+3][j+7]=v8;
v1=A[m+4][j];v2=A[m+4][j+1];v3=A[m+4][j+2];v4=A[m+4][j+3];
v5=A[m+4][j+4];v6=A[m+4][j+5];v7=A[m+4][j+6];v8=A[m+4][j+7];
B[m+4][j]=B[m][j+4];B[m+4][j+1]=B[m+1][j+4];B[m+4][j+2]=B[m+2][j+4];B[m+4][j+3]=B[m+3][j+4];
B[m][j+4]=v1;B[m+1][j+4]=v2;B[m+2][j+4]=v3;B[m+3][j+4]=v4;B[m+4][j+4]=v5;
B[m+4][j+5]=v6;B[m+4][j+6]=v7;B[m+4][j+7]=v8;
v1=A[m+5][j];v2=A[m+5][j+1];v3=A[m+5][j+2];v4=A[m+5][j+3];
v5=A[m+5][j+4];v6=A[m+5][j+5];v7=A[m+5][j+6];v8=A[m+5][j+7];
B[m+5][j]=B[m][j+5];B[m+5][j+1]=B[m+1][j+5];B[m+5][j+2]=B[m+2][j+5];B[m+5][j+3]=B[m+3][j+5];B[m+5][j+4]=B[m+4][j+5];
B[m][j+5]=v1;B[m+1][j+5]=v2;B[m+2][j+5]=v3;B[m+3][j+5]=v4;B[m+4][j+5]=v5;B[m+5][j+5]=v6;
B[m+5][j+6]=v7;B[m+5][j+7]=v8;
v1=A[m+6][j];v2=A[m+6][j+1];v3=A[m+6][j+2];v4=A[m+6][j+3];
v5=A[m+6][j+4];v6=A[m+6][j+5];v7=A[m+6][j+6];v8=A[m+6][j+7];
B[m+6][j]=B[m][j+6];B[m+6][j+1]=B[m+1][j+6];B[m+6][j+2]=B[m+2][j+6];B[m+6][j+3]=B[m+3][j+6];
B[m+6][j+4]=B[m+4][j+6];B[m+6][j+5]=B[m+5][j+6];
B[m][j+6]=v1;B[m+1][j+6]=v2;B[m+2][j+6]=v3;B[m+3][j+6]=v4;B[m+4][j+6]=v5;B[m+5][j+6]=v6;
B[m+6][j+6]=v7;B[m+6][j+7]=v8;
v1=A[m+7][j];v2=A[m+7][j+1];v3=A[m+7][j+2];v4=A[m+7][j+3];
v5=A[m+7][j+4];v6=A[m+7][j+5];v7=A[m+7][j+6];v8=A[m+7][j+7];
B[m+7][j]=B[m][j+7];B[m+7][j+1]=B[m+1][j+7];B[m+7][j+2]=B[m+2][j+7];B[m+7][j+3]=B[m+3][j+7];
B[m+7][j+4]=B[m+4][j+7];B[m+7][j+5]=B[m+5][j+7];B[m+7][j+6]=B[m+6][j+7];
B[m][j+7]=v1;B[m+1][j+7]=v2;B[m+2][j+7]=v3;B[m+3][j+7]=v4;B[m+4][j+7]=v5;B[m+5][j+7]=v6;B[m+6][j+7]=v7;
B[m+7][j+7]=v8;
}
else
{
for(k=i;k<i+8;k++)
for(m=j;m<j+8;m++)
B[m][k]=A[k][m];
}
259次,比理论最小miss值多3次,这是函数调用造成的,可以用csim
程序进行输出找到调用情况。此时已经是它的最优化状态了。
64×64
分析
从32×32
矩阵来看,主要解决了以下问题,实现了优化
- 分块大小,使得分块内部不产生冲突
- 对角线上,A的上一次加载会被B替换,A需要再加载一次。
- 对角线上,B的上一次加载会被A替换,B需要再加载一次。
64×64
的优化也是这些内容,只是方式不同而已。
分块大小
Cache一行可以存8个int值,一共能存64×64
矩阵的4行。可以这样分块
- 分块大小
4×4
。优点:64×64
矩阵的第5行不会造成冲突,实现了分块内部不冲突。缺点:对于矩阵A而言,4×4
分块时,读取一个分块中一行的一个元素,实际上也会加载下一个分块的一行,这样才能存满Cache一行,它可不知道你是用了4×4
分块,这样对A没有影响,每行后四列下次可以继续使用。
但是,对于B而言,4×4
分块会浪费每一行的后四列
for(i=0;i<N;i+=4)
for(j=0;j<M;j+=4)
for(m=i;m<i+4;m++)
{
v1=A[m][j+0];
v2=A[m][j+1];
v3=A[m][j+2];
v4=A[m][j+3];
B[j+0][m]=v1;
B[j+1][m]=v2;
B[j+2][m]=v3;
B[j+3][m]=v4;
}
- 分块大小
8×8
。最大的问题是后4行会替换掉前4行,分块内部有冲突。
for(i=0;i<N;i+=8)
for(j=0;j<M;j+=8)
for(m=i;m<i+8;m++)
{
v1=A[m][j+0];
v2=A[m][j+1];
v3=A[m][j+2];
v4=A[m][j+3];
v5=A[m][j+4];
v6=A[m][j+5];
v7=A[m][j+6];
v8=A[m][j+7];
B[j+0][m]=v1;
B[j+1][m]=v2;
B[j+2][m]=v3;
B[j+3][m]=v4;
B[j+4][m]=v5;
B[j+5][m]=v6;
B[j+6][m]=v7;
B[j+7][m]=v8;
}
- 分块大小
8×8
内分块成4个4×4
.弥补“1.” “2.”
中的的缺点,既不分块内部冲突,也充分利用Cache一行存8个int的特性。
但我们需要对其进行处理,旨在尽可能用完Cache一块,且降低miss。
解决
我们使用分块大小8×8
内分块成4个4×4
的方式进行处理。
- 将A的左上和右上一次性复制给B
- 用本地变量把B的右上角存储下来
- 将A的左下复制给B的右上
- 利用上述存储B的右上角的本地变量,把A的右上复制给B的左下
- 把A的右下复制给B的右下
for(i=0;i<N;i+=8)
for(j=0;j<M;j+=8)
{
//get A's first 4 rows
for(m=i;m<i+4;m++)
{
v1=A[m][j+0];
v2=A[m][j+1];
v3=A[m][j+2];
v4=A[m][j+3];
v5=A[m][j+4];
v6=A[m][j+5];
v7=A[m][j+6];
v8=A[m][j+7];
B[j+0][m]=v1;
B[j+1][m]=v2;
B[j+2][m]=v3;
B[j+3][m]=v4;
B[j+0][m+4]=v5;
B[j+1][m+4]=v6;
B[j+2][m+4]=v7;
B[j+3][m+4]=v8;
}
//save B2 /get A3
for(m=j;m<j+4;m++)
{
//B2
v1=B[m][i+4];
v2=B[m][i+5];
v3=B[m][i+6];
v4=B[m][i+7];
//A3.at this time,A3 A4 have all been load
v5=A[i+4][m];
v6=A[i+5][m];
v7=A[i+6][m];
v8=A[i+7][m];
//A3 to B2
B[m][i+4]=v5;
B[m][i+5]=v6;
B[m][i+6]=v7;
B[m][i+7]=v8;
//B2 to B3
//now B3 B4 one line have been load
B[m+4][i+0]=v1;
B[m+4][i+1]=v2;
B[m+4][i+2]=v3;
B[m+4][i+3]=v4;
}
//A4 to B4
for(m=i+4;m<i+8;m++)
{
v1=A[m][j+4];
v2=A[m][j+5];
v3=A[m][j+6];
v4=A[m][j+7];
B[j+4][m]=v1;
B[j+5][m]=v2;
B[j+6][m]=v3;
B[j+7][m]=v4;
}
已经拿到满分。
理论最小miss值优化
和矩阵32×32
一样,64×64
存在理论最小值。64×64
分成64个8×8
分块,A B中每一个分块都是8个冷不命中,共64×8×2=1024
次。
如果要优化这里也要进行展开手动转置,展开代码太多且代码难以阅读(我实在卷不动了)。
![](https://i-blog.csdnimg.cn/blog_migrate/7cb71d19b77bd75fbd2d517fa1be9322.png)
但是,我还发现一篇使用“借用区块”而非展开达到1024理论值miss的文章《CSAPP - Cache Lab的更(最)优秀的解法》
61×67
因为本题miss放得很宽松,<2000即可满分。使用16×16或者17×17分块即可通过。其实还有更细致的方法使得miss更少,但,卷不动了。
解决
for(i=0;i<N;i+=17)
for(j=0;j<M;j+=17)
for(m=i;m<N&&m<i+17;m++)
for(n=j;n<M&&n<j+17;n++)
{
B[n][m]=A[m][n];
}
实现文件
Note:driver.py 是用python2版本的语法(print “”),用python3会出错,最好指明使用python2 运行
相关文件已上传到网盘。
PartA PartB C文件-微云
PartA PartB C文件-CSDN免积分
总结
《CS:App》配的Lab太好了,看完书过了1个月才来写CacheLab,发现啥思路也没有(也可能是习题没做)。果然看完书不代表就会了,必须要自己上机实践才能有更深的理解,与现实应用连接。