实验准备
这里有几份资料可以学习 cache
的工作原理:
- CMU 课程 slide
- 现代操作系统—原理与实现 page 18
- CS: APP page 424
对于实验来说,请仔细阅读 Write up
文档,如果需要,请安装下载 Valgrind
(安装方式请自行 STFW
)
我更换了我的实验环境,从
WSL2
迁移到了真实的Linux
平台上,但对于这份实验来说并没有什么影响
Part A
要求
实现一个命令行工具 csim
,用于模拟 cache
的工作原理,其官方版本的功能如下:
其中,参数 s E b
的含义如图:
写命令行工具时,我们可以通过
#include <unistd.h>
提供的getopt()
函数来解析命令行参数,这在前文的 slide 中有提到
Data Structure
首先,我们通过上图来设计 cache
的数据结构,显然这是一个矩阵,每一个条目视为一个 struct
,其中包括 valid bit
, tag
, block
,当然,我们还需要实现 LRU
算法,因此这里我们还需要一个时间戳来记录出现的时间。
于是,可以设计数据结构如下:
struct cache_line { u_int16_t valid; u_int32_t tag; u_int32_t *block; u_int32_t timestamp;} **cache;
注意到这里的
block
我用了数组,实际上甚至可以不需要用这个成员,因为模拟器并不会存储任何数据,只是模拟hit
,missing
,evict
而已。
Control Flow
我们可以设计如下控制流:
sequenceDiagram main()->>parsing_cmd(): "Parsing Command Line" activate parsing_cmd() parsing_cmd()-->>main(): "Some Result" deactivate parsing_cmd() main()->> eval(): "start to simulate" activate eval() loop UNTIL EOF eval() ->> get_page(): "get page from disk or cache" get_page() -x eval(): "finish" end eval()-->>main(): "finish" deactivate eval()
分治后,就可以开始解决各个子问题了。
parsing_cmd
实际上我们可以在 main
中实现这部分逻辑:
struct cache_line { u_int16_t valid; u_int32_t tag; u_int32_t *block; u_int32_t timestamp;} **cache;
int S, s, E, b, verbose;
u_int32_t hits, misses, evictions;
char *trace_file;
int main(int argc, char *argv[]) { int opt; while (-1 != (opt = getopt(argc, argv, "hvs:E:b:t:"))) { switch (opt) { case 's': s = atoi(optarg); S = (1 << s); break; case 'E': E = atoi(optarg); break; case 'b': b = atoi(optarg); break; case 't': trace_file = optarg; break; case 'h': fprintf(stdout, "Usage: ./%s [-hv] -s <s> -E <E> -b <b> -y <tracefile>\n \ -h: Optional help flag that print usage info\n \ -v: Optional verbose flag that displays trace info\n \ -s <s>: Number of set index bits (S = 2^s is the number of sets)\n \ -E <E>: Associativity (number of lines per set)\n \ -b <b>: Number of block bits (B = 2^b is the block size)\n \ -t <trace_file>: Name of the valgrind trace to replay\n", argv[0]); exit(0); case 'v': verbose = 1; break; default: fprintf(stderr, "Usage: ./%s [-hv] -s <s> -E <E> -b <b> -y <tracefile>\n", argv[0]); exit(-1); } }
// Allocate cache cache = (struct cache_line **)malloc(sizeof(struct cache_line *) * S);
eval();
// Free cache for (int i = 0; i < S; i++) { free(cache[i]); } free(cache);
printSummary(hits, misses, evictions);
return 0;}
如果不会使用
getopt
,可以看 slide 或者man 3 getopt
看手册学
大概的骨架并不需要太多解释,我们的重点应该放在 eval
函数中。
eval
由于我们不需要处理除 S
, L
, M
外的其他字符开头的行(注意你可能会读到 \r\n
等字符),并且对每次 S
L
M
的操作是类似的,都是 get_page()
,于是,代码是显然的:
void eval() { FILE *file = fopen(trace_file, "r"); char identifier; u_int64_t address; u_int32_t size;
while (fscanf(file, "%c %lx,%d", &identifier, &address, &size) != EOF) { switch (identifier) { case 'I': case ' ': case '\n': case '\r': break; case 'L': get_page(identifier, address, size); break; case 'S': get_page(identifier, address, size); break; case 'M': get_page(identifier, address, size); get_page(identifier, address, size); break; default: fprintf(stderr, "Unrecognized identifier: %c\n", identifier); exit(-1); } }}
注意,这里的 M
代表修改,所以我们需要先 L
一次,然后 S
一次,这样就需要更新(或者是 get
)两次 page
而对于 get_page
,逻辑也是显然的:
- 通过地址获取
set_index
,tag
,offset
三个值 - 查询是否为
cold miss
,即cache[set_index] == NULL
(cache 中一条数据都没有) - 如果是,则直接从
disk
中调入页面,在这里,我们只是把cache[set_index]
中的一行的tag
设置为该地址的tag
,并将其设置为有效 - 如果不是,那么我们查看是否已经被调入到
cache
中来了 - 如果是,则已经找到,将时间戳设置为最新后,更新所有条目的时间戳,然后返回
- 否则,我们需要从
disk
中调入页面
而在调入页面时,我们需要判断此时的 cache
中是否已经满了(检查是否所有有效位均为 1 即可),如果是,那么我们必须驱除出去一页,才能进行调入。
代码如下:
void get_page(char identifier, u_int64_t address, u_int32_t size) { u_int32_t offset = address & ((1 << b) - 1); u_int32_t set_index = (address >> b) & ((1 << s) - 1); u_int32_t tag = address >> (b + s);
if (verbose) { printf("%c %lx,%d ", identifier, address, size); }
if (cache[set_index] == NULL) { cache[set_index] = (struct cache_line *)malloc(sizeof(struct cache_line) * E); insert_page(set_index, tag, offset); } else {
for (int i = 0; i < E; i++) { if (cache[set_index][i].valid && cache[set_index][i].tag == tag) { hits++; cache[set_index][i].timestamp = 1; update_cache(); if (verbose) { printf("hit "); } return; } }
insert_page(set_index, tag, offset); }
putchar('\n');}
void update_cache() { for (int i = 0; i < S; i++) { struct cache_line *set = cache[i]; if (set == NULL) continue; for (int j = 0; j < E; j++) { if (set[j].valid) { set[j].timestamp++; } } }}
void insert_page(u_int32_t set_index, u_int32_t tag, u_int32_t offset) { misses++; printf("miss "); struct cache_line *set = cache[set_index]; for (int i = 0; i < E; i++) { if (set[i].valid == 0) { set[i].valid = 1; set[i].tag = tag; set[i].timestamp = 1; set[i].block = malloc(sizeof(u_int32_t) * (1 << b)); set[i].block[offset] = 1; update_cache(); return; } } evict_page(set_index, tag, offset);}
void evict_page(u_int32_t set_index, u_int32_t tag, u_int32_t offset) { evictions++; printf("eviction "); struct cache_line *set = cache[set_index]; int max = 0; int max_index = 0; for (int i = 0; i < E; i++) { if (set[i].timestamp > max) { max = set[i].timestamp; max_index = i; } } set[max_index].valid = 1; set[max_index].tag = tag; set[max_index].timestamp = 1;}
注意这里的
offset
其实一点用都没有,当然为了模拟还是让他有点作用了。我们在
evict_page
的最后,直接将tag
修改为需要调入页面的tag
,这样省去了再次get_page
的痛苦。
结果
Part B
这部分想要做出来是简单的,但想要做满分是很难的,后面会详细写一份调优的博客。
最简单的方法就是使用分块,考虑到这里的 cache
是 32 字节的,一个 int
为 4 字节,所以我们可以八个八个值的来复制,于是代码应运而生:
char transpose_submit_desc[] = "Transpose submission";void transpose_submit(int M, int N, int A[N][M], int B[M][N]) { int tmp0, tmp1, tmp2, tmp3, tmp4, tmp5, tmp6, tmp7; for (int i = 0; i < M; i += 8) { for (int j = 0; j < N; j += 8) { for (int k = i; k < i + 8; k++) { tmp0 = A[k][j]; tmp1 = A[k][j + 1]; tmp2 = A[k][j + 2]; tmp3 = A[k][j + 3]; tmp4 = A[k][j + 4]; tmp5 = A[k][j + 5]; tmp6 = A[k][j + 6]; tmp7 = A[k][j + 7];
B[j][k] = tmp0; B[j + 1][k] = tmp1; B[j + 2][k] = tmp2; B[j + 3][k] = tmp3; B[j + 4][k] = tmp4; B[j + 5][k] = tmp5; B[j + 6][k] = tmp6; B[j + 7][k] = tmp7; } } }}
当然,这份代码是没办法达到最好效果的,32×32
矩阵的理论 miss 值为 ,而我们的结果为
如果用这个代码去考虑 64×64
的矩阵,那么 miss 会达到惊人的 ,显然是无法接受的,这里的做法可以是减少分块的大小,例如我们用 4×4
的分块,那么就可以减少 miss 的值: