实验目的与要求
1、让学生更好地应用程序性能的优化方法;
2、让学生更好地理解存储器层次结构在程序运行过程中所起的重要作用;
3、让学生更好地理解高速缓存对程序性能的影响;
实验原理与内容
本实验将帮助您了解缓存对C程序性能的影响。实验由两部分组成。在第一部分中,您将编写一个模拟高速缓存行为的小型C程序(大约200-300行)。在第二部分中,您将优化一个小的矩阵转置函数,目标是最小化缓存未命中的数量。
1、第一部分:编写缓存模拟器
cachelab-handout/traces子目录包含一组内存引用轨迹文件,这些文件将用于评估您在第一部分中编写的缓存模拟器的正确性。内存引用轨迹文件由Linux程序valgrind生成,具有如下格式:
I 0400d7d4,8
M 0421c7f0,4
L 04f6b868,8
S 7ff0005c8,8
每行表示一次或两次内存访问,每一行的格式如下:
[空格]操作地址,大小
操作字段表示内存访问的类型:“I”表示指令加载,“L”表示数据加载,“S”表示数据存储,“M”表示数据修改(即,数据加载+数据存储)。在每个“I”的前面是没有空格的,在每个“M”、“L”和“S”的前面都有一个空格。“操作地址”字段代表一个64位十六进制内存地址。“大小”字段指定对应操作所访问的字节数。
在第一部分中,您将编辑csim.c文件,实现一个缓存模拟器。它以valgrind的内存引用轨迹文件作为输入,模拟缓存的命中和未命中行为,并输出命中、未命中和逐出的数量。
我们为您提供了一个名为csim-ref的标准的缓存模拟器的二进制可执行文件,该模拟器模拟具有任意大小和相联度的缓存的行为。在选择要逐出的缓存行时,它使用LRU替换策略,这个参考模拟器就是标准答案,它可以接收以下命令行参数:
./csim-ref [-hv] -s <s> -E <E> -b <b> -t <tracefile>
-h: 可选参数,用于输出使用帮助
-v:可选参数,用于输出跟踪信息的详细内容
-s <s>:设置组索引位的数量(组数量S=2s)
-E <E>:设置每组的行数
-b <b>:设置块大小(块大小B=2b位)
-t <tracefile>:要跟踪的内存引用轨迹文件的名称
命令行参数基于教科书第六章的符号(s、E和b),例如执行指令“./csim-ref -s 4 -E 1 -b 4 -t traces/yi.trace”将输出“yi.trace”内存引用轨迹文件在标准缓存模拟器(s=4,E=1,b=4)中的缓存使用情况:
第一部分的工作是要编辑csim.c文件,以使它使用与csim-ref相同的命令行参数和产生与它相同的输出。请注意,拿到手上的csim.c几乎完全为空,你需要从头开始写。
第一部分的注意事项:
- 你的csim.c文件必须在没有警告的情况下通过编译才能获得分数。
你的模拟器必须在任意的s、E和b下都能正常工作。这意味着你需要使用malloc函数为模拟器的数据结构分配空间。malloc函数的使用方法自己百度。
- 对于这个实验中,我们只对数据缓存性能感兴趣,所以您的模拟器应该忽略所有指令缓存访问(以“I”开头的行)。“I”总是顶格的,它前面没有空格,而“M”、“L”和“S”前面有空格,可以根据这一点来区分。
- 要获得第一部分的分数,必须在main()函数结束前调用函数printsummmary()来输出命中、未命中和驱逐的数量。
- 对于这部分,您应该假设内存访问已正确对齐,因此内存访问永远不会跨越块边界。通过这个假设,你可以忽略在内存访问轨迹文件中的“大小”字段。
2、第二部分:优化矩阵转置
在第二部分中,你将编辑trans.c文件实现一个转置函数,目标是使得在程序运行过程中尽可能少的发生缓存未命中。
如果A表示一个矩阵,则矩阵A的转置矩阵为AT,它们之间的关系为:Aij=ATji
为了帮助您编程,我们在trans.c中为您提供转置函数示例,该示例计算N×M矩阵A的转置,并将结果存储在M×N矩阵B中。该示例的转置函数是正确的,但它效率低下,因为访问模式导致许多缓存未命中。
在第二部分中,您的工作是编写一个类似的函数,称为transpose_submit,以缓存未命中最少化的方式实现矩阵转置。
第二部分注意事项:
1)你的trans.c文件必须在没有警告的情况下通过编译才能获得分数。
2)每个转置函数最多可以定义12个int类型的局部变量。
3)不允许通过使用long类型的变量或使用位技巧将多个值存储到单个变量这种手段来规避2)中的限制。
4)转置函数不能使用递归。
5)如果使用函数调用,在被调用函数和顶层转置函数之间的栈空间上的局部变量不得超过12个。例如,如果你的转置函数声明8个变量,然后在转置函数里调用了一个使用4个变量的函数,该函数再调用另一个包含2个局部变量的函数,则栈空间上将有14个变量,这将违反规则。
6)您的转置函数不能修改数组A,但是您可以修改数组B。
不允许在代码中定义任何数组或使用malloc之类的函数。
实验设备与软件环境
1.Linux操作系统—64位 Ubuntu 18.04
2. C编译环境(gcc)
3. 计算机
实验过程与结果(可贴图)
1.安装valgrind,在终端命令行中运行指令“sudo apt-get install valgrind”
2.安装Python2.7,在终端命令行中运行指令“sudo apt-get install python”
3.将“cachelab-handout.tar”文件拷贝到虚拟机的某文件夹里面并在该文件夹里打开终端使用命令 “tar xvf cachelab-handout.tar”进行解压
A部分
在csim.c中完成cache模拟器的编码。自己写的程序需要输出与目标文件一样。cache使用LRU策略。先查看一下分数
查看一下dave.trace
valgrind生成的内存引用轨迹文件格式,每行代表一次或两次内存访问,包括操作类型(如"I"、"L"、"S"、"M"分别表示指令加载、数据加载、数据存储和数据修改)以及相应的64位十六进制内存地址和访问字节数。
编写代码
1.每行含有:一个有效位、t个标记位,b个高速缓存位。以及支持LRU的时间。可以定义一个struct支持。
2.根据给定的命令行参数 -s、-E 和 -b 设置缓存系统的组索引位数、每组缓存行数和块大小,可以定义一个二维数组。
完整代码如下:
27满分
B部分
在trans.c中完成矩阵转置函数transpose_submit,并且越少的Miss越好。
限制:
评估方法规则如下:
32 × 32: 满分8 分 if 未命中 < 300, 0 points if 未命中 > 600
64 × 64: 满分8 分 if 未命中 < 1, 300, 0 points if 未命中 > 2, 000
61 × 67: 满分10 分 if 命中 < 2, 000, 0 points if 未命中 > 3, 000
1、32 × 32
对于题目所给的trans()函数来说,misses数高的原因在于,对于数组A是以行来访问,而对于数组B是以列为访问,又由cahce的存储量可知,一整个cache可以存储的数组的前8行所有元素(8行填满一个cahce),而在访问数组B第九行的第1个元素之后,又会将之前存储的八行cache全部冲突替换掉,导致没有充分利用cache数据(只用到每个块的1个元素),只能重新加载之前的cache,造成大量的misses。
为了提高cache的利用率,即,在cache载入后,将cache包含的元素全部操作后再替换cache,保证不会二次载入相同的cache,即设置子块大小为 8 × 8
- 最多使用12 local variables int。不能作弊。
- 不可递归。
- 用helper函数的话,栈总共不超12 variables
- A不许修改。
- 不允许用数组和malloc.
运行结果会发现会有343次的misses,而理论上的研究则为 16 块 × 8 次 × 2 = 256 次 16块 \times 8次 \times 2 = 256次 16块×8次×2=256次,显然有很大的差距,而且满分的操作为misses < 300。再次分析trace文件就会发现数组A(0x30a080)和B(0x34a080)的起始地址所映射的cache块相同,即在数组A和B的对角块上的元素会发生冲突不命中,而且在对角块上时数组B的缓存会将刚才缓存的数组A丢弃掉,故我们只需将A中缓存的值用变量保存起来,就可以减少misses数。
显然可以看出对角块数组A和B的缓存会存在冲突
通过./csim-ref -v -s 5 -E 1 -b 5 -t trace.f0 > trace_details.f0
命令查看一下命中情况
故改进代码如下:
2、64 × 64
如果采用刚才同样的分析,可以得到子块为 8 × 4,可以保证数组B每四个cache块( 4 × 8),不会发生二次载入的情况。而对于数组A来说,四个cahce块为( 8 × 4),这样的配置会导致每一个A的cache块只有四个int数据会被利用到,而其余四个数据需要下次载入才可利用,这样的代码如下:
misses数为1651,很显然不符合满分要求。(misses < 1300)
所以为了能够充分利用cache块,我们只能在 8 × 8 8\times8 8×8的框架下具体分析操作。(将 8 × 8 8\times8 8×8分为4个 4 × 4 4\times4 4×4)
思路:为了将前文浪费的四个int数据有效利用起来,因为局部变量数目的限制,所以可以考虑将多的数据暂时放入数组B的cache中,以待后续的操作,这样就可以避免二次载入相同的cache块
① 观察以下两个对应的 8 × 8 8\times8 8×8区域。我们要将A的元素转置到B
② 将区域一的黄色区域元素转置至对应位置,将区域一的蓝色区域暂时转置存放在区域二的蓝色区域(即数组B此时cache块的右半部分)
③ 而后逐行进行后四行前四列的转置
至此这个 8 × 8 8\times8 8×8的区域全部转置完成,理论上每一块中不命中一次,即 8 块 / 行 × 64 行 × 2 = 1024 次 8块/行\times64行\times2 = 1024次 8块/行×64行×2=1024次。
3、61 × 67
此时所给的M和N对于cache块来说已经无法像前面的情况一样,可以对齐处理,如果要分析的话比较复杂,题目的满分要求也比较低misses < 2000。故采用变换分块大小来观察。
基本上 8 × 8 8\times8 8×8之后misses数在2000左右浮动,没有什么规律,在 17 × 17 17\times17 17×17时达到最小1950。
总的分数为:
实验总结
本次CacheLab实验中,我编写了缓存模拟器,模拟内存引用轨迹,遵循LRU策略,支持多参数设置,编译无警告,与标准模拟器对比验证准确。针对32×32、64×64、61×67矩阵,优化转置函数以减少缓存未命中的次数。对32×32矩阵,设8×8子块、用临时变量避免对角块冲突,未命中降为343次(满分要求<300次)。对64×64矩阵,初试8×4子块后未达标(1651次),改用4个4×4子块,优化数据访问,满足满分要求(<1300次)。虽然这个实验看上去不难,但是我还是需要借助一定的资源完成该实训。