CacheLab 实验报告
本实验分为两部分,第一部分要求模拟一个cache,第二部分要求根据给定的缓存结构优化矩阵转置函数。
Part A: Writing a Cache Simulator
1. 任务说明与初步思路
根据实验文档,在本部分,我们要在csim.c
中编写代码,以valgrind memory trace
文件作为输入,在控制台输出总的hit
, miss
和eviction
数,实现handout中csim-ref
的效果:
linux> ./csim-ref -v -s 4 -E 1 -b 4 -t traces/yi.trace
L 10,1 miss
M 20,1 miss hit
L 22,1 hit
S 18,1 hit
L 110,1 miss eviction
L 210,1 miss eviction
M 12,1 miss eviction hit
hits:4 misses:5 evictions:3
linux> ./csim-ref -h
• -h: Optional help flag that prints usage info
• -v: Optional verbose flag that displays trace info
• -s <s>: Number of set index bits (S = 2s is the number of sets)
• -E <E>: Associativity (number of lines per set)
• -b <b>: Number of block bits (B = 2b is the block size)
• -t <tracefile>: Name of the valgrind trace to replay
而Valgrind memory trace
文件内容格式如下:
I 0400d7d4,8
M 0421c7f0,4
L 04f6b868,8
S 7ff0005c8,8
/* Each line denotes one or two memory accesses. The format of each line is
[space(only for 'I')]operation address,size */
拆分本任务,可从本任务中提取几个关键部分:
- 读取命令行参数;
- 模拟高速缓存的数据结构;
- 读取
trace
文件并模拟缓存过程。
通读实验报告后,我们可以获得下面几个提示:
- 只需要模拟
S(data store)
,L(data load)
和M(data modify, i.e. data load following by data store)
这三个指令; - 缓存采用
LRU(least-recently used)
的替换策略来驱逐缓存块; - 由于缓存结构由命令行参数决定,故会用到
malloc()
函数; - 输出采用
printSummary
函数输出,-v
和-h
命令行参数可选择不实现; - 可以采用
unistd.h
与getopt.h
中的getopt()
函数读取命令行参数,可参考man 3 getopt
。
在写代码之前,会有如下问题:
- 如何管理可变个命令行参数?
- 使用
getopt()
函数。
- 使用
S
,L
和M
的指令有何区别?- 本例中
S
和L
指令没有区别,都是检查缓存中是否有地址中的元素,有则hit
无则miss
后载入缓存; M = L + S
,故事实上第二个S
一定会hit
;- 容易迷惑的点:
miss
后会把相应数据载入缓存,但不会再访问缓存命中并打印hit
;eviciton
后会用LRU
策略驱逐块,之后会再次访问缓存不命中、再次打印miss
,而同样地,miss
后会把相应数据载入缓存,但不会再访问缓存命中打印hit
。
- 本例中
- 选用什么数据结构来实现
cache
,需要实现cache
的哪部分功能?- 初步思路是用结构体实现
cacheline
,再以二维数组的形式来组织成cache
; cache
的功能不必完全实现,由于不需要存入具体的数据,因此在cacheline
结构体中不必添加B
和用于存放数据的成员数组cacheblock
;- 可以注意到,参数
b
的唯一作用就是计算标记位t
的值来解析地址,与b
相关的参数也不必集成在cache
结构体中。
- 初步思路是用结构体实现
下面分部分完成本任务。
2. 实验过程
部分 1 - 读取命令行参数
根据实验文档,我们要接收的命令行参数为:
Usage: ./csim [-hv] -s <s> -E <E> -b <b> -t <tracefile>
我们采用getopt
函数解析命令行参数,使用方法可参考官方文档。其中,为了方便调试,在这里我们实现-v
功能,用一个全局变量int showDetails
来判断未来模拟过程中是否输出细节信息。
int showDetails = 0;
void readCommandline(int argc, char* argv[], int *s, int *E, int *b, char *tracefile){
const char *optstr = "hvs:E:b:t:";
int opt;
while((opt = getopt(argc, argv, optstr)) != -1){
switch(opt){
case 'h':
printHelpMenu(); // 打印帮助菜单
exit(0);
case 'v':
showDetails = 1; // 设置全局变量showDetails为真
break;
case 's':
*s = atoi(optarg); // 外部变量 optarg - 指向当前选项参数字符串的指针
break;
case 'E':
*E = atoi(optarg);
break;
case 'b':
*b = atoi(optarg);
break;
case 't':
strncpy(tracefile, optarg, strlen(optarg));
break;
}
}
}
部分 2 - 模拟高速缓存的数据结构
高速缓存的最小结构单元是行,即cacheline
,它可以用结构体来实现,含有以下成员变量:
typedef struct Cache{
int valid; // 有效位的值
unsigned tag; // 标记位的值
int recentTimeUsed; // 用于LRU策略,记录最后一次被访问的'时间'
}Cacheline;
E
个cacheline
组合成一个set
,S
个set
组合成一个cache
。因此,cache
可视为是一个S*E
的cacheline
二维数组。为了还要集成其他的有关cache
的信息如S
, E
和B
,我们把cache
也做成一个结构体,成员变量如下:
typedef struct Cache{
int S, E;
Cacheline **cachelines; // cacheline的二维数组
}Cache;
下面为Cacheline
和Cache
依次编写初始化方法。
void initCacheline(Cacheline *cacheline){
cacheline->valid = 0;
cacheline->tag = 0;
cacheline->recentTimeUsed = 0;
}
void initCache(int S, int E, Cache *cache){
cache->S = S;
cache->E = E;
cache->cachelines = (Cacheline **)malloc(sizeof(Cacheline *)*S);
for(int i=0;i<S;i++){
cache->cachelines[i] = (Cacheline *)malloc(sizeof(Cacheline)*E);
// ★易错!注意 &cache->cachelines[i][j] 和 cache->cachelines[i]的关系
for(int j=0;j<E;j++){
initCacheline(&cache->cachelines[i][j]);
}
}
}
由于初始化Cache
对象的过程中用了动态内存分配,需要为Cache
编写释放内存的函数:
void freeCache(Cache *cache)