CSAPP:CacheLab实验

趁期末考试复习了《深入理解计算机系统》第六章,进一步了解了cache的原理。想着写篇博客帮助巩固一下。有些地方写得可能不是很好,希望多多包涵,同时也欢迎指出。

cachelab一共分为两部分,PartA是让你模拟cache运行的过程,就是模拟cache的行为。PartB是一个矩阵转置,给出了三种规模,你的任务就是尽可能的提高高速缓存的命中率,它会根据你的miss,hits,eviction这三个值的大小进行打分,评分的范围已给出。

好啦实验介绍完毕。下面开始进入正式环节。

PartA

这是原版文档的要求。大概就是告诉你它只是一个模拟缓存的行为并不是真实缓存,内存的数据不用存储,不使用块偏移,因此地址种b并不重要,简单的计算hits、miss、evictions的值。然后就是你的模拟器需要适用于不同的(s,b,E),同时给出运行时间。最后就是要求你使用这个LRU最近最少使用策略,意思就是使用这个策略进行行的牺牲,因此cache发生miss的时候,就会从它的k+1层存储器读取新的拷贝,假设在空间已满的情况下(未满的时候不需要替换行),新读取的数据需要存储在cache里面,但空间已满,只能选择驱逐(原文是驱逐的意思,也就是覆盖某一行),然后覆盖的方法就是这个LRU策略。LRU策略就是:选择那个最后被访问的时间距离现在最远的块。代码体现的话就是给cache的每一行绑一个Lrunumber,这个值越小(你也可以设置为最大,随自己喜欢)表示它最后一次被访问的时间距离现在最远,可以作为牺牲行。

一、那么模拟一个cache的行为需要用到哪些变量?

cache有组数S、一组包含的行数E,存储块的字节大小B,cache的容量C(C=S*E*B)

地址的构成:标识位t、组索引s、块偏移b(前面说了实验不使用块偏移,但计算标记位的时候需要用到)

示意图:


我简单解释下上面说的这些变量:

首先是cache的构成:

cache有2^s组,每组有E行(E分为三种情况:E=1,称为直接映射高速缓存;1<E<C/B:组相联高速缓存;E=C/B,全相连高速缓存,也就是一组包含了所有的行),然后是每行的构成:(1)有效位vild(取0和1,1表示存储了有效信息,0表示没有,判断命中的时候需要用到)(2)标记位t=m-(s+b),当cpu要读取某个地址的内容时,就会将某个地址的第一个部分:标记位与它进行对比,匹配相等则命中,也就是锁定在某一行(3)数据块B,B负责存储这个地址的内容,把B想象成字节数组就好,共有B-1个小块(别和B这个数据块搞混咯,下标从0开始,因此是B-1)。在这里简单提下b的作用吧,虽然实验没用到。b理解为数据块B这个数组的下标就好,也就是你要从B[b]这个位置开始读取字。注意这里的数据都是2进制的。

以上这些都可以从那个原版的PPT读取,只是我讲得更详细些,这也是我自己的理解。

二、自带函数的介绍:

cache.h里面自带了一些函数,PPT给出了要用到的几个的介绍:

1.getopt()

如果函数声明丢失,则在Unix命令行上自动解析元素,通常在循环中调用以检索参数,它的返回值存储在局部变量中,

当getopt()返回-1时,没有更多的选项。(百度翻译的)一句话就是它是解析你的那个命令行的。

2.fscanf()

读入测试文件的,自带有调用就好了。具体使用方式如下:(写的时候粘贴复制一下就好了。)

FILE * pFile; //pointer to FILE object
pFile = fopen ("tracefile.txt",“r"); //open file for reading
char identifier;
unsigned address;
int size;
// Reading lines like " M 20,1" or "L 19,3"
while(fscanf(pFile,“ %c %x,%d”, &identifier, &address, &size)>0){
// Do stuff
}

fclose(pFile); //remember to close file when done

3.Malloc/free

分配和释放内存空间的函数。

具体用法如下:

Some_pointer_you_malloced = malloc(sizeof(int));

Free(some_pointer_you_malloced);

分配了内存用完的时候记得释放,还有不要释放没有分配的内存。

三、根据(一)可以写出下面结构体:(基于c语言)

定义行的属性:

typedef struct{

    int valid;       //有效位
    int tag;          //标识位
    int LruNumber;   //牺牲行的时候要用的,具体上面说了。
} Line;

定义组的属性:
typedef struct{
    Line* lines;    //用于存储一组中包含的行
} Set;

定义cache的属性:
typedef struct {
    int set_num;    //组数
    int line_num;   //行数
    Set* sets;      //cache的空间,模拟cache
} Sim_Cache;

四、函数实现:

1.getopt(),命令行解析函数,主要用来解析你输入的那个命令。命令中包含的关键字就那么几个,s,E,b,v(是否打印标记位和组索引)atoi函数是将字符转换成整数的函数。


2.文本处理函数,也就是读入测试文件的,直接用它自带的fscanf就好了。在cache.h那个头文件里面。

3.检查参数合法性:主要目的就是检查输入的命令是否合法。


4.打印帮助文档的函数,当输入的命令出错的时候,运行这个函数,也就是说你解释一下这些关键字代表的意思就好啦。


5.更新Lru计数值的函数:访问第xE行,根据标记为将该行的LRU设置为最大值,然后其它行用一个for循环减一即可。


6.查找牺牲行的函数:

遍历找出第x组,找到最小的LRU所在的行,那么该行就是牺牲行。关于怎么找,无非就算设置标记位,不断比较和更新即可。


7.判断是否命中:命中在前面说了,要满足两个条件:(1)设置了有效位;(2)请求的地址的标记位与cache地址的标记位一致。最后记得更新一下Lru计数值。


8.更新高速缓存cache的函数:cache的容量有限,当满的时候需要牺牲行(或者说驱逐某行),先遍历当前组,判断它满了没有,如何判断是否满,可以遍历所有的行,只要有一个有效位为0,(有效位的作用是说明该行是否存储了数据,通俗的理解就是是否为空)则该组未满。如果没有满的话:将有效位不为1的那一行更新有效位为1,同时更新标记位和LRU计数值。如果满了,找出最小LRU所在的行作为牺牲行。



9.读取数据的函数,亦即操作L:指导PPT上说命中则hit++,不命中则miss++且如果牺牲行eviction++,如果有v指令就打印访问结果。那么可以用一个标记来判断是否有-v指令。


10.存储数据的函数,亦即操作S:原理跟L操作一样也是修改标记位和组索引指令的,然后调用一下L操作那个函数即可。


11.修改数据的函数,亦即操作M:这个操作实际就是执行了一次L和一次S。


12.获取标记位和组索引:前面说了一个当要访问一个地址(暂且称为请求地址吧)的内容时,这个请求地址是这样的结构:标记位(t) 组索引(s) 块偏移(b),然后cache中地址的结构:有效位(v) 标记位(v) 数据块(B)。我们现在的任务是从这个请求地址中获取t和s,才能从cache中寻找需要访问的内容。地址位数未知,但问题不大,比如我求标记位的时候直接将它右移到最右边即可,组索引由于右移的时候带着标记位,因此要和一个数与运算一下,把它“剪”下来。



13.最后写一个初始化cache的函数就好了。


14.main函数代码:


hit、miss、eviction三个变量定义成全局好些。

然后贴一下运行的结果图:(对比标准的缓存与自己写的缓存,引用标准的缓存程序加-ref)


yi.trace的内容如下:


它就是测试了这7条指令。下面我们来简要分析一下:

1.对于地址0x10进行访问:(标蓝的表示组索引)
x10=0000...0001 0000,块偏移值为最低四位,故组索引s=1;
一开始的时候cache是空的,因此第一次访问的时候为miss
2.执行指令M 20,连续对地址0x20进行连续两次访问:
0x20=000...0010 0000,组索引s=2;
所以第一次访问的时候没有要的内容,访问结果为miss;然后cache从低一级存储器读取第一次访问需要的内容,第二次访问的时候有需要的内容且标记位相等,所以第二次访问的时候结果为hit
3.对地址0x22进行访问:
0x22=000...0010 0100,组索引s=2;
由于操作2以将该块存入高速缓存且标记位都相等,故结果为hit;

4.对地址0x18进行访问:

0x18=000...0001 0100,组索引s=1;

由于之前的操作,该块已存入高速缓存且标记位都为0,故访问结果为hit

5.对地址0x110进行访问:

0x110=000...0001 0001 0000 故组索引s=1;

操作4将该块存入高速缓存了但标记位不相等,故访问结果为miss,发生一次eviction

6.对地址0x210进行访问:

0x210=000...0010 0001 0000,故组索引s=1,这里标记位为2跟操作5读取新的行的标记位不匹配,故访问结果为miss,cache读取新的行,发生一次eviction

7.对地址0x12进行连续两次访问:

0x12=000...0001 0010,组索引s=1;

第一次访问结果为miss,因为在操作6的时候发生了一次行替换把该块驱逐了即使标记位相等,cache重新读取该块,发生一次eviction,那么第二次肯定为hit。(跟操作1类似的)

故总共:hits=4 ; miss=5;eviction=3;

下面是打分的运行图:

运行指令./test-csim:


可以看到跟标准的缓存一样,但还有地方可以优化,期待大佬指出!

PartB:

PartB我就不做过多的赘述了。

以下是PartB的一些要求:

1.最多只能定义 12 个局部变量

2.不使用任何的数组,不调用任何类似于 malloc 的开辟内存的函数

3.测试矩阵的规模为 32 × 32,64 × 64,61 × 67

4.测试 cache 的组成:32 组, 每组一行,每个块 32 字节。对于编写的函数,miss 个数越少越好

PartB打分规则如下:

矩阵转置让我联想到上次perlaba实验对图像旋转时用的分块处理方法,这里可分成32x32,16x16,8x8,4x4,cache每块的字节数是32,也就是8个int,因此为了有效地利用cache的块容量,选取8x8的分块效果是最好的。

首先编写处理32x32的函数:

  分块后,对角线上的块由于位置没有改变,所以需要另外处理。(否决了)尝试了很久之后我换一种方法,就是我直接将每一块的八个int直接取出来就好了。就算是对角线的元素也满足。代码如下:

运行结果如图:


满分,效果不错,是否64x64也能继续沿用这种办法呢?试一试咯。

 当矩阵规模增到到64x64的时候,由于矩阵规模比较大,无法在cache的一行全部加载矩阵的一行,还是先分成8x8的块。我处理是思路是这样的:每次处理一个块,也就是像32x32那样取出每行的8个元素,然后将 每行的0-3号元素正常的放到B[0-3][0]这些位置去(也就是正常的转置),剩下的四个元先放置B矩阵4x4块的最右边保存,注意这个时候我已经取出来了。然后再用一个循环去把这些值放到B正确的位置去,然后A[4]A[7]也是上述的处理。对整个矩阵重复这个操作即可。简单的示意图如下:


但是这个方法有个缺点,就是越到后面用来存储最后四个元素的空间就越少(这个肯定是一个能优化的地方,我暂时没想到如何解决,期待大佬指出!)

参考代码如下:


运行结果如图:


有大佬优化那个miss到1123,我相信我这个优化了上面说的那个问题也能达到那个值。又是一个满分,因为不满分不做下一个2333。

最后是61x67,乍一看这玩意的size有点稀奇,还想引用刚才的分块大小,但是给了我一个invild!不得不放弃这种办法了,后来想过增大分块的大小,之前说8x8的分块效果是最好的,那只是针对32x32或者64x64这种跟cache的容量大小成倍数关系的矩阵,而且也不一定非要用8x8的分块啊!后来我调整了分块的大小,测试了四组,最终确定17是最好的。

贴图贴图:

1.8x8:


1913还能接受,起码得到满分了。但还可以继续优化!

2.16x16:


emmm,下降明显!继续增大看看。

3.17x17:


下降的幅度很小了,所以到这里差不多了,但还是增大看看。

4.18x18:


大于18的都不用考虑,这里开始miss上升了。

然后贴个最终的运行截图吧。

运行命令:./driver.py:


有写得不好的地方欢迎指出。

欢迎转载,创作不易,请务必注明出处谢谢。




















猜你喜欢

转载自blog.csdn.net/weixin_42294984/article/details/80738945
今日推荐