这个是做CacheLab的记录,part A的内容是做一个和csim-ref一样功能的cache模拟器,使用LRU替换策略。
有一说一第一次看到这个lab的时候还是吓一跳的,因为题目说是修改csim.c文件即可,我一开始以为是文件里框架什么的都给好了只需要适当填空就行,没想到打开后发现只有一句printSummary()函数。
不过其实慢慢做下来发现也没有想象中复杂,最近寒假每天抽出来四五个小时,三天就写完了part A。
但是part B的blocking技术那块现在理解的还不是很好,而且也不知道如何处理任意行列的矩阵转置的优化这个问题。因此也就还没有做,以后有时间再回头写写吧。
cache结构
不论是全相联、组相联还是直接映射,cache都可以用一个四元组(S, E, B, m)来表示,其中一个cache中有S = 2s个组,每组有E行,每行(块)的大小是B = 2b字节。一个地址有m位,其格式为:
t位 | s位 | b位 |
---|---|---|
标记CT | 组索引CI | 块偏移CO |
LRU替换策略
LRU的实现是这样的:对每个行设置一个LRU标记位LRU_tag
。每次访问cache时将新加入或命中的行的LRU_tag置为0,并将其他行的LRU_tag加一。当需要替换时,将LRU_tag最大的行换出。
相对应的,LFU主要是从次数来考虑,而LRU主要是从“上次访问的时间”来考虑。
命令行参数getopt()
首先要考虑的是命令行参数,这个可以使用在<unistd.h>
中的getopt()
函数来实现。具体使用方法网上都可以找到。
需要注意的是我在实现的时候会报错说getopt()函数未声明,查阅后只需添加头文件<getopt.h>
即可。
字符串分割strtok()
trace文件里读取address,size
格式的数据,需要用到字符串分割。本来在网上搜到说以前使用的strtok()
是线程不安全的,后来都要使用strsep()
。但我在实现的时候使用strsep()
会报错,查找后知道这并不是一个标准里的函数,有些地方可能没有支持,因此最终还是使用了strtok
来分割字符串。
访问cache
无论是S、L还是M指令,它们对cache的访问行为都是相同的,即检查cache中是否有需要的地址。只有M特殊在它相当于是L+S,因此它的第二次访问是一定会命中的。
因此可以把三个指令抽象为一个相同的访问cache的行为。
代码
下面是我的实现代码。
#include "cachelab.h"
#include <unistd.h>
#include <getopt.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
void print_help(char *argv[]) {
printf("Usage: %s [-hv] -s <num> -E <num> -b <num> -t <file>\n", argv[0]);
printf("Options:\n");
printf(" -h Print this help message.\n");
printf(" -v Optional verbose flag.\n");
printf(" -s <num> Number of set index bits.\n");
printf(" -E <num> Number of lines per set.\n");
printf(" -b <num> Number of block offset bits.\n");
printf(" -t <file> Trace file.\n");
printf("Examples:\n");
printf(" linux> %s -s 4 -E 1 -b 4 -t traces/yi.trace\n", argv[0]);
printf(" linux> %s -v -s 8 -E 2 -b 4 -t traces/yi.trace\n", argv[0]);
}
/*
* cache的结构
* 每个cache由若干组组成,每个组由若干行组成
* 因此先定义cache行,再定义cache组,最后组成cache
*/
// 写这个的时候才知道C语言不用typedef而只使用struct会出错
typedef struct {
int is_valid;
int LRU_tag;
unsigned long tag; // 使用unsigned long而非long,原因在主函数中提及
char *blocks; // char等价于byte,块大小取决于参数b
}Cache_line;
typedef struct {
Cache_line *lines; // 行数量取决于参数E
}Cache_set;
typedef struct {
Cache_set *sets; // 组数量取决于参数s
}Cache;
/*
* cache的“构造”和“析构”函数
* 动态分配、回收cache
* 使用calloc而非malloc来初始化内存
*/
Cache cache_constructor(int s, int E, int b) {
int S = 1 << s;
int B = 1 << b;
Cache cache;
// cache组
cache.sets = (Cache_set*)calloc(S, sizeof(Cache_set));
// 每组的行
for (int i = 0; i < S; i++) {
cache.sets[i].lines = (Cache_line*)calloc(E, sizeof(Cache_line));
// 每行的块
for (int j = 0; j < E; j++) {
cache.sets[i].lines[j].blocks = (char*)calloc(B, sizeof(char));
// 由于使用calloc,所以就不需要对每一行都初始化了
}
}
return cache;
}
void cache_destructor(Cache *cache_ptr, int s, int E, int b) {
int S = 1 << s;
for (int i = 0; i < S; i++) {
for (int j = 0; j < E; j++)
free(cache_ptr->sets[i].lines[j].blocks);
free(cache_ptr->sets[i].lines);
}
free(cache_ptr->sets);
}
/*
* 递增每行的LRU_tag位
*/
void incLRUtag(int s, int E, Cache *cache_p) {
int S = 1 << s;
for (int i = 0; i < S; i++)
for (int j = 0; j < E; j++)
cache_p->sets[i].lines[j].LRU_tag++;
}
int main(int argc, char *argv[]) {
// 检查并输入命令行参数
char c; // 单个字符
int s, E, b; // cache参数
int v = 0; // v = 1表示verbose模式
char *file = NULL; // trace文件路径
while ((c = getopt(argc, argv, "hvs:E:b:t:")) != -1) {
switch (c) {
case 'v':
v = 1;
break;
case 's':
s = atoi(optarg);
break;
case 'E':
E = atoi(optarg);
break;
case 'b':
b = atoi(optarg);
break;
case 't':
file = optarg;
break;
case 'h':
default:
print_help(argv);
return 0;
}
}
// 打开并读取tracee文件
FILE *fp = NULL;
// 如果文件读取失败
if ((fp = fopen(file, "r")) == NULL) {
printf("%s: No such file or directory\n", file);
return 0;
}
// 创建一个cache
Cache cache = cache_constructor(s, E, b);
// hit、miss和eviction数量
int hit_N = 0, miss_N = 0, evic_N = 0;
// 循环读取trace文件里除了I(instruction load)的其他指令
while ((c = fgetc(fp)) != EOF) {
char str[30]; // 存储每行的字符串
// 除了I之外的每个指令都有一个前置空格
if (c != ' ') {
// 跳过该行
fgets(str, 30, fp);
continue;
}
// 其他指令
c = fgetc(fp); // 读取指令字符到c
fgetc(fp); // 跳过指令字符后的空格
fgets(str, 30, fp); // 读取剩下的格式为"address,size"的内容到str
// 题目说可以忽略size,读取的地址存在address_str里
char *address_str = NULL;
address_str = strtok(str, ",");
/*
* strtol()的第二个参数是char **endptr,它保存着截断处的字符串地址
* 如strtol(str, &size_str, 16);即保存','开始的后面的字符串
* 所以可以使用这个特性来保存字符串size,但为了方便就先没有实现这个
*/
long address = strtol(address_str, NULL, 16);
// 使用unsigned long而非long是为了获得标记和组索引时使用逻辑右移而非算术右移,这样的话不会被符号位影响
unsigned long tag = address >> (s + b); // the tag bits
// 这里的setIndex是指集合的索引,不是设置索引
unsigned long setIndex = (unsigned long)address << (64 - s - b) >> (64 - s);
//long blockOffsef = address << (64 - b) >> (64 - b);
int flag = -1; // flag = 1指hit, = 0指miss, = 2指eviction
// 访问cache
for (int i = 0; i < E; i++) {
// 命中时
if (cache.sets[setIndex].lines[i].tag == tag && cache.sets[setIndex].lines[i].is_valid == 1) {
// 设置LRU标记位和flag
incLRUtag(s, E, &cache);
cache.sets[setIndex].lines[i].LRU_tag = 0;
flag = 1;
break;
}
}
// 不命中,存到cache中
if (flag != 1) {
for (int i = 0; i < E; i++) {
// 如果有空行,就写到这里
if (cache.sets[setIndex].lines[i].is_valid == 0) {
incLRUtag(s, E, &cache);
cache.sets[setIndex].lines[i].LRU_tag = 0;
cache.sets[setIndex].lines[i].is_valid = 1;
cache.sets[setIndex].lines[i].tag = tag;
flag = 0;
break;
}
}
// 否则使用LRU策略替换
if (flag != 0) {
int maxLRUline = 0;
for (int i = 0; i < E; i++)
if (cache.sets[setIndex].lines[i].LRU_tag > cache.sets[setIndex].lines[maxLRUline].LRU_tag)
maxLRUline = i;
incLRUtag(s, E, &cache);
cache.sets[setIndex].lines[maxLRUline].LRU_tag = 0;
cache.sets[setIndex].lines[maxLRUline].is_valid = 1;
cache.sets[setIndex].lines[maxLRUline].tag = tag;
flag = 2;
}
}
// S、L和M都有相同的行为(M多加一次一定命中的访问)
// 根据flag递增hit、miss和evition的数量
switch (flag) {
case 1:
hit_N++;
break;
case 0:
miss_N++;
break;
case 2:
miss_N++;
evic_N++;
break;
}
// verbose模式
if (v == 1) {
printf("%c %s, ", c, address_str);
switch (flag) {
case 1:
printf("hit");
break;
case 0:
printf("miss");
break;
case 2:
printf("eviction");
break;
}
}
// 如果是指令M,那么第二次访问一定命中
if (c == 'M') {
hit_N++;
if (v == 1) {
printf(" hit\n");
}
}
else
printf("\n");
}
// 释放资源
fclose(fp);
cache_destructor(&cache, s, E, b);
printSummary(hit_N, miss_N, evic_N);
return 0;
}