# 实验准备
这里有几份资料可以学习 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值为 $256$,而我们的结果为 $288$
如果用这个代码去考虑 64×64
的矩阵,那么 miss 会达到惊人的 $4612$,显然是无法接受的,这里的做法可以是减少分块的大小,例如我们用 4×4
的分块,那么就可以减少 miss 的值: