CSAPP Cache Lab
Posted joker D888
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了CSAPP Cache Lab相关的知识,希望对你有一定的参考价值。
CSAPP Cache Lab
本实验将帮助您了解缓存存储器对 C 语言性能的影响程式。实验室由两部分组成。 在第一部分中,您将编写一个小的 C 程序(大约 200-300 行)模拟高速缓存的行为。 在第二部分中,您将优化一个小型矩阵转置函数,目标是最小化高速缓存未命中的次数。
Part A: Writing a Cache Simulator
在 A 部分中,您将在 csim.c
中编写一个缓存模拟器,它将 valgrind
内存跟踪作为输入,在此跟踪上模拟高速缓存的命中/未命中行为,并输出命中,未命中和驱逐总数。实验为我们提供了参考缓存模拟器的二进制可执行文件,称为 csim-ref
,它在 valgrind
跟踪文件上模拟具有任意大小和关联性的缓存行为。 它使用选择要逐出的缓存行时的 LRU(最近最少使用)
替换策略。关于csim-ref
的详细信息可以查看官方文档,或自动测试。
我们在这部分的工作是填写 csim.c
文件,以便它采用相同的命令行参数和产生与参考模拟器csim-ref
相同的输出。
-
查看官方实验文档,得知
csim-ref
最多有6个命令行参数,所以首先需要处理命令行参数的读入,可以使用getopt
方便的处理。完整代码中这部分模块如下,先了解框架即可。// 使用getopt函数,每次取出一个命令行参数 while ((opt = getopt(argc, argv, "hvs:E:b:t:")) != -1) switch (opt) case 'h': printf("%s", help_info); exit(EXIT_SUCCESS); break; case 'v': debug = true; break; case 's': s = atoi(optarg); break; case 'E': E = atoi(optarg); break; case 'b': b = atoi(optarg); break; case 't': trace_file = optarg; break; default: // error input fprintf(stderr, "Usage: %s [-hv] -s <num> -E <num> -b <num> -t <file>\\n", argv[0]); exit(EXIT_FAILURE); break;
-
读入的命令行参数中有高速缓存参数,
s E b
分别为组索引位数量(2s为组数),每组行数,块偏移位数量(2b为块大小单位字节),和书上的内容一致。同时我们需要设置每一行的结构,如标记,组索引,块偏移(实际用不到),以及一些其他变量,如hit_cnt, miss_cnt, eviction_cnt
命中,未命中,驱逐个数,等等。一些设置如下。extern char* optarg; // getopt设置的全局变量 bool debug = false; // 命令行参数v的标记,显示跟踪信息的表示,默认关闭 int s, E, b; // 缓存参数:组索引位数量(2^s为组数),每组行数,块偏移位数量(2^b为块大小单位字节) char* trace_file; // 输入来源文件 int hit_cnt, miss_cnt, eviction_cnt; // 命中,未命中,驱逐 个数 int cur_time = 0; // 时间计数(用于LRU算法) char opera = 0; // 运算符 uint64_t address = 0; // 地址 int size = 0; // 大小 // 缓存行结构 typedef struct Cacheline bool vaild; // 有效位 int tag, time_stamp; // 标记位,时间戳(用于LRU算法) Cache; Cache* cache; // 便于cache的访问 #define cache(x, y) cache[x * E + y] // const int kMaxSize = 64; c语言不能用const常量定义数组大小 #define kMaxSize 64 char input_str[kMaxSize]; // 存储读入的一行数据 const char* debug_info[3] = "miss", "miss hit", "miss eviction"; // 跟踪信息
-
接下来需要根据命令行参数获取的
trace_file
进行操作的读取。首先根据文档我们不用处理I
操作,进一步实际上我们只是需要模拟高速缓存的行匹配机制和不匹配时加载即可,并不用真的进行数据的读取和存储,所以L
操作和S
操作是一样的。而且文档说M
操作只是先数据加载,再数据存储,所以M
操作也可以变为L
操作和S
操作。最终我们要做的只有一个操作。下面是操作的读取以及行匹配以及不匹配时加载的操作。//行匹配以及不匹配时加载操作 void load(uint64_t address, bool display) ++cur_time; // 每次load时间都会++ int tag = address >> (b + s); // 标记 int index = (address >> b) & ((1 << s) - 1); // 组索引 // 遍历第index组,查看是否命中 for (int i = 0; i < E; ++i) if (cache(index, i).vaild && cache(index, i).tag == tag) ++hit_cnt; cache(index, i).time_stamp = cur_time; if (display) printDebug(0); return; // 不命中-- ++miss_cnt; // 判断是否有“空行” for (int i = 0; i < E; ++i) if (!cache(index, i).vaild) // cache(index, i) = true, tag, cur_time; C 语言不允许这样整体赋值 cache(index, i).vaild = true; cache(index, i).tag = tag; cache(index, i).time_stamp = cur_time; if (display) printDebug(1); return; // 行替换(LRU) int last_stamp = cur_time, pos = -1; ++eviction_cnt; for (int i = 0; i < E; ++i) if (cache(index, i).time_stamp < last_stamp) last_stamp = cache(index, i).time_stamp; pos = i; // cache(index, pos) = true, tag, 0; cache(index, pos).vaild = true; cache(index, pos).tag = tag; cache(index, pos).time_stamp = cur_time; if (display) printDebug(2); // 工作函数,读所给文件,进行处理 void work() FILE* fp = fopen(trace_file, "r"); // 读形式打开文件 while (fgets(input_str, kMaxSize, fp) != NULL) sscanf(input_str, " %c %lx,%d", &opera, &address, &size); // 即使I运算符前面有空格也可以正常读入 switch (opera) case 'I': continue; // 指令加载 忽略不在我们的处理范围 break; case 'L': // 数据加载,即读 case 'S': // 数据存储,即写 load(address, true); // 我们模拟的高速缓存的读和写实际上是相同的操作 break; case 'M': // 数据修改,即先数据加载再数据存储 load(address, true); // 执行两次即可 load(address, false); break; default: break; fclose(fp); free(cache);
-
最后,根据模块的功能不同,封装了两个函数,
init
函数处理命令行参数的解析,work
函数处理操作的读入以及操作的执行。最终完整代码如下。160行拿下,实际上不用对命令行参数h
和v
进行相应也是可以通过测试的。代码的整体设计还是比较满意的,模块的功能分明,逻辑也比较清晰。#include <getopt.h> #include <stdbool.h> #include <stdint.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include "cachelab.h" extern char* optarg; // getopt设置的全局变量 bool debug = false; // 命令行参数v的标记,显示跟踪信息的表示,默认关闭 int s, E, b; // 缓存参数:组索引位数量(2^s为组数),每组行数,块偏移位数量(2^b为块大小单位字节) char* trace_file; // 输入来源文件 int hit_cnt, miss_cnt, eviction_cnt; // 命中,未命中,驱逐 个数 int cur_time = 0; // 时间计数 char opera = 0; // 运算符 uint64_t address = 0; // 地址 int size = 0; // 大小 // 缓存行结构 typedef struct Cacheline bool vaild; // 有效位 int tag, time_stamp; // 标记位,时间戳(用于LRU算法) Cache; Cache* cache; // 便于cache的访问 #define cache(x, y) cache[x * E + y] // const int kMaxSize = 64; c语言不能用const常量定义数组大小 #define kMaxSize 64 char input_str[kMaxSize]; // 存储读入的一行数据 const char* debug_info[3] = "miss", "miss hit", "miss eviction"; // 跟踪信息 // 初始化,用于参数的读入,以及高速缓存结构的建立 void init(int argc, char* argv[]) const char* help_info = "Usage: ./csim [-hv] -s <num> -E <num> -b <num> -t " "<file>\\nOptions:\\n -h Print this help message.\\n -v " "Optional verbose flag.\\n -s <num> Number of set index " "bits.\\n -E <num> Number of lines per set.\\n -b <num> " "Number of block offset bits.\\n -t <file> Trace file.\\n\\n" "Examples :\\n linux> ./csim -s 4 -E 1 -b 4 -t " "traces/yi.trace\\n linux> ./csim -v -s 8 -E 2 " "-b 4 -t traces/yi.trace\\n "; int opt = 0; // 使用getopt函数,每次取出一个命令行参数 while ((opt = getopt(argc, argv, "hvs:E:b:t:")) != -1) switch (opt) case 'h': printf("%s", help_info); exit(EXIT_SUCCESS); break; case 'v': debug = true; break; case 's': s = atoi(optarg); break; case 'E': E = atoi(optarg); break; case 'b': b = atoi(optarg); break; case 't': trace_file = optarg; break; default: // error input fprintf(stderr, "Usage: %s [-hv] -s <num> -E <num> -b <num> -t <file>\\n", argv[0]); exit(EXIT_FAILURE); break; cache = (Cache*)malloc(sizeof(Cache) * (1 << s) * E); memset(cache, 0, sizeof(Cache) * (1 << s) * E); void printDebug(int status) // 0 命中 1 未命中 2 驱逐 printf("%c %lx,%d %s\\n", opera, address, size, debug_info[status]); //行匹配以及不匹配时加载操作 void load(uint64_t address, bool display) ++cur_time; // 每次load时间都会++ int tag = address >> (b + s); // 标记 int index = (address >> b) & ((1 << s) - 1); // 组索引 // 遍历第index组,查看是否命中 for (int i = 0; i < E; ++i) if (cache(index, i).vaild && cache(index, i).tag == tag) ++hit_cnt; cache(index, i).time_stamp = cur_time; if (display) printDebug(0); return; // 不命中-- ++miss_cnt; // 判断是否有“空行” for (int i = 0; i < E; ++i) if (!cache(index, i).vaild) // cache(index, i) = true, tag, cur_time; C 语言不允许这样整体赋值 cache(index, i).vaild = true; cache(index, i).tag = tag; cache(index, i).time_stamp = cur_time; if (display) printDebug(1); return; // 行替换(LRU) int last_stamp = cur_time, pos = -1; ++eviction_cnt; for (int i = 0; i < E; ++i) if (cache(index, i).time_stamp < last_stamp) last_stamp = cache(index, i).time_stamp; pos = i; // cache(index, pos) = true, tag, 0; cache(index, pos).vaild = true; cache(index, pos).tag = tag; cache(index, pos).time_stamp = cur_time; if (display) printDebug(2); // 工作函数,读所给文件,进行处理 void work() FILE* fp = fopen(trace_file, "r"); // 读形式打开文件 while (fgets(input_str, kMaxSize, fp) != NULL) sscanf(input_str, " %c %lx,%d", &opera, &address, &size); // 即使I运算符前面有空格也可以正常读入 switch (opera) case 'I': continue; // 指令加载 忽略不在我们的处理范围 break; case 'L': // 数据加载,即读 case 'S': // 数据存储,即写 load(address, true); // 我们模拟的高速缓存的读和写实际上是相同的操作 break; case 'M': // 数据修改,即先数据加载再数据存储 load(address, true); // 执行两次即可 load(address, false); break; default: break; fclose(fp); free(cache); int main(int argc, char* argv[]) init(argc, argv); work(); printSummary(hit_cnt, miss_cnt, eviction_cnt); return 0;
使用unix> make && ./test-csim
或 unix> make && ./driver.py
测试得到 Part A 27分满分,通过全部测试。这部分比较简单。
Part B: Optimizing Matrix Transpose
做这部分时需要确保你已经安装了valgrind
,可以valgrind --version
查看是否安装,否则安装sudo apt install valgrind
(Ubuntu 20.04下)。
在 B 部分中,我们将在 trans.c
中编写一个转置函数,它会导致尽可能少的缓存未命中。我们在 trans.c
中为您提供了一个示例转置函数trans
,用于计算N × M 矩阵 A 的转置并将结果存储在 M × N 矩阵 B 中。你在 B 部分的工作是编写一个类似的函数,称为 transpose_submit
,它最小化数字不同大小矩阵的缓存未命中数。
注意,其中有一些限制规则需要我们遵守,详见实验文档。
实验所给的cache参数为s = 5, E = 1, b = 5
,即共有32组,每组一行,每行32字节(存8个int)。共有32 × 32 (M = 32, N = 32) 64 × 64 (M = 64, N = 64) 61 67 (M = 61, N = 67) 三类举证大小。
高速缓存cache大小为32×32=1024字节,对于一个32×32的矩阵A来说的话,能存下前8行,但是另外还有一个矩阵B,我们需要思考对于A[x][y]
和B[x][y]
是否会是同一个组吗。通过test-trans
程序生成的文件trace.fi
可以发现问题的答案是肯定的,会映射在同一个组。下面是我们观察例子函数trans
对应的trace.f1
,我们查看第一条B[i][j]=A[i][j]
,即可发现A[0][0]
和B[0][0]
分别为0010d080
和0014d080
,根据组索引即可判断。(查看实验文档获得相应提示研究可知)
根据文档提示,可以使用书上提到的分块来提高时间局部性。
32×32
我们先考虑32×32的矩阵,由于每行可以存储8个int,所以我们使用8×8的分块大小进行处理。设计如下。miss次数为287小于300,拿下满分。
for (int i = 0; i < N; i += 8) // 块起点x轴
for (int j = 0; j < M; j += 8) // 块起点y轴
for (int k = i; k < i + 8; ++k) // 每次处理一行
// 下面8个读入,只有第一个会miss,其他七个都会hit
int tmp1 = A[k][j];
int tmp2 = A[k][j + 1];
int tmp3 = A[k][j + 2];
int tmp4 = A[k][j + 3];
int tmp5 = A[k][j + 4];
int tmp6 CSAPP Lab4 Cache Lab