Cache Lab记录

by Gu Wei
2021年11月

实验分为两部分,Part A要求实现一个cache的模拟器,Part B要求实现一个为特定cache优化过的转置矩阵函数。

1. Part A

输入的trace文件都是由valgrind生成,如:linux> valgrind --log-fd=1 --tool=lackey -v --trace-mem=yes ls -l可以依次输出内存的访问。trace的格式为:

“I” denotes an instruction load, “L” a data load, “S” a data store, and “M” a data modify (i.e., a data load followed by a data store). There is never a space before each “I”. There is always a space before each “M”, “L”, and “S”. The address field specifies a 64-bit hexadecimal memory address. The size field specifies the number of bytes accessed by the operation.

现在给出s、E、b这些参数以及trace文件,我们需要输出hit、miss以及eviction的次数。替换策略采用的是LRU(least-recently used)。用法具体如下:

输出的例子如下:

注意到加了v参数就会多输出一些信息。

要模拟cache,其实参数b以及trace文件的I都是没有用的,而trace文件的M、L、S其实也是同质化的。M就是查两次cache有没有,L和S则是一次。我并没有采取队列来实现LRU,而是用了时间戳,感觉队列会比较麻烦。

话不多说,直接上代码:

关键就是getopt和fscanf两个函数需要学习一下。sdyt,这是本人第一次正式写C程序

测试如下:

得分27就对了。


 

2. Part B

Part B要求我们实现trans.c文件中的transpose_submit函数,对32*32,64*64,61*67三个矩阵进行特殊的转置优化。cache大小为s = 5, E = 1, b = 5。即,直接映射Cache,有32组,每组一行,每行能放32个字节(8个int)。

评分要求为:

2.1 32*32

1)naive-plus solution

最直接的理解为:

测试出miss数为1183,远远大于要求的300(为什么要运行这么久才能测出来)

解释:

以i=0,j=0~31的例子说明。cache每个block放8个int,A矩阵按行读入,第一行产生32/8=4次miss。B矩阵按列写入,每次写都会产生miss,共有32次。在i=1的时候,写入B矩阵的第1列,仍会产生32次miss,这是由cache只有32行所导致的。这样算出来有36*32=1152次miss,测试却是1183。这是产生了冲突不命中(抖动)的原因。对于AB矩阵主对角线上的元素会映射到同一cache块上面,这是由AB的地址所导致的(这一点不查别人的资料很难想到)。也就是说读A[0][0]的时候产生一次miss,然后把A[0][0]到A[0][7]的元素存到了cache的中;接下来把A[0][0]写入B[0][0]中,但是在写入的时候会把B[0][0]到B[0][7]的元素存到cache之前存了A[0][0]到A[0][7]的那一行中,接下来读取A[0][1]的时候,又要把A[0][0]到A[0][7]存回该block中。这便产生了冲突不命中。

具体查看生成的trace.f0文件有:

从第五行开始看,A首地址在0x10c080、B首地址在0x14c080。这样对角线的元素会产生抖动,增加一次miss,对角线共32个元素,那么1152+32=1184,和实际结果的1183十分接近。算是一个不错的估算了,可能中间还有些细节没有考虑到。

2) naive solution

方法1每次写入B矩阵都产生了miss,而方法2利用分块技术来避免写入B矩阵时产生的大量miss。采取的分块是8*8的,一方面,每个cache块存8个int;另一方面两个8*8的块用16个cache块,不会产生容量不命中。而且8又是32的约数,对编码来说比较简单。于是有:

测试出miss数为343。还是不能达到满分。

解释:

对于主对角线上的8*8分块,仍然会产生冲突不命中。对于不在主对角线上的12个块共有16*12次miss。对于在主对角线上的四个块,第0行10次miss、第1~6行4次,第7行3次,这样共有37*4次miss。算出来是16*12+4*37=340。与343很接近。不排除算错

3) acceptable solution

我们知道在主对角线上的块会产生冲突也就是:当把A[0][0]写入B[0][0]时,会把B[0][0]到B[0][7]的元素存到cache之前存了A[0][0]到A[0][7]的那一行中,接下来读取A[0][1]的时候,又要把A[0][0]到A[0][7]存回该block中。那么直接在读A[0][0]的时候顺便把A[0][1]到A[0][7]都读了不久可以解决问题了吗。于是有:

测试结果有miss数为287。

解释:1216+424=288。优化掉了读取A时候的冲突不命中,但是在写入B的时候还是有8*4次额外的冲突不命中。但是已经满分了。

理论上最小的miss数为16*16=256次,但是没有必要再去琢磨了。

2.2 64*64

上来就高兴地用刚刚的8*8分块一跑,miss数高达4611次。朴素的做法也只有4723次miss。。。问题出在写入B时产生了冲突。比如说写入B[0][0]~B[0][7]的时候依次写入了set0、set8、set16、set24、set0、set8、set16、set24这样便产生了冲突。

那用4*4分块跑,如下:

miss数还是达1699。这主要由于对块的利用不高导致,一个cache块可以存8个int,但是每次只用4个。

于是就gg了。百度 启动!

这个miss数是1243,已经满分了。相当tricky的解决办法。大体思路:通过把8*8的块分成四个4*4的块进行处理,即充分利用了cache块可以存八个int的能力,又避免了写入B产生的冲突。本代码来自CS:APP3e 深入理解计算机系统_3e CacheLab实验 - 李秋豪 - 博客园 (cnblogs.com),博客里面有详细的图片解释,可以学习一下。本人只是意会了一下

2.3 61*67

这里要求放的很宽,只要小于2000就行,下面这样的16*16分块就行了。

miss数1992,刚刚够!


 

3. 写在后面

本人于2021/11/2完成了Cache Lab,差不多用了四天时间吧。Cache Lab算是目前最有代码量的一个lab了。Part A要求实现cache simulator,完全是自己写的代码(不像arch lab那样),整个完成的思路还是比较清晰的。Part B很多还是要参考网上的写法,尤其是64*64的分块还是可以继续深入下去玩味的。总的来说,Cache Lab也挺不错的!

个人主要强化了一下知识:

有意思的是:

预计接下来: