csim.c
实现一个cache,make
然后./test-csim
测试是否正确trans.c
实现一个转置操作,并优化性能,测试方法如下 make && ./test-trans -M 32 -N 32
make && ./test-trans -M 64 -N 64
make && ./test-trans -M 61 -N 67
首先,这个实验就是要求我们能够得出在一系列操作之下,命中次数,不命中次数,淘汰页面的次数。
这个模拟cache的功能非常简单,因为不需要我们真正的去读写数据,只需要模拟进出cache的情况就可以了。因此,我们每个cache行,只需要像下面这样定义即可。
typedef struct {
int valid;
int tag;
int time_stamp;
} cache_line;
然后我们需要支持三种操作,分别是load,store和modify
综上所述,load和store的实现完全一样,modify相当于再操作一次。对应在代码里就是这样。
这个函数的第一行和第二行就是使用位运算取出了这个地址的set和tag
void Func(char command, int address, int size) {
int set_num = (address >> b) & ((1 << s) - 1);
int tag = (address >> (b + s)) & ((1 << (32 - b - s)) - 1);
FindCache(set_num, tag);
if (command == 'M') {
FindCache(set_num, tag);
}
}
通过这个函数可以发现,其实真正关键的函数是FindCache函数。
这个函数其实就是遍历这个对应的set的所有行
void FindCache(int set_num, int tag) {
cache_line *cur_set = cache[set_num];
int empty_index = -1;
int min_ts_line_index = 0;
for (int i = 0; i < E; i++) {
// 当前行存在于cache中,即valid=1,并且tag相同
if (cur_set[i].valid == 1 && cur_set[i].tag == tag) {
hit_count++;
// 记得更新时间戳
cur_set[i].time_stamp = time_stamp++;
return;
}
// 如果存在空行,即valid=0;
if (cur_set[i].valid == 0) {
empty_index = i;
}
// 记录时间戳最小的,实在不行就要去替换了
if (cur_set[i].time_stamp < cur_set[min_ts_line_index].time_stamp) {
min_ts_line_index = i;
}
}
// 如果没有命中,那么就发生了miss
miss_count++;
// 载入某一行,如果有空行,就载入空行
// 如果没有空行,则载入时间戳最小的,这就发生了evict
if (empty_index != -1) {
LoadOrEvict(cur_set, empty_index, tag);
} else {
LoadOrEvict(cur_set, min_ts_line_index, tag);
eviction_count++;
}
}
这个实验的关键部分就这么多。剩下的有点类似于脏活累活,但是也很有意义。先看一下整体的代码结构
可以分为以下几个部分
int main(int argc, char *argv[]) {
// 解析命令行参数
int par_res = parser(argc, argv);
if (par_res == -1) {
return 1;
}
// 创造cache,初始化cache
Init();
// 读取文件,获得操作,进行操作
int get_res = GetOperation();
if (get_res == -1) {
return 1;
}
// 输出结果
printSummary(hit_count, miss_count, eviction_count);
// 释放malloc申请的变量
Destory();
return 0;
}
首先是我们通过命令行启动程序,那么我们的参数都是在命令行中给出的,如何解析命令行的参数呢?需要使用getopt
函数。
这个函数的三个参数
./my_program -f input.txt -o output.txt
,那么argv的实际参数是这样的argv[0] -> "./my_program"
argv[1] -> "-f"
argv[2] -> "input.txt"
argv[3] -> "-o"
argv[4] -> "output.txt"
int parser(int argc, char *argv[]) {
// 解析参数
int opt;
while ((opt = getopt(argc, argv, "hvs:E:b:t:")) != -1) {
switch (opt) {
case 'h':
h_flag = 1;
break;
case 'v':
v_flag = 1;
break;
case 's':
s = atoi(optarg);
break;
case 'E':
E = atoi(optarg);
break;
case 'b':
b = atoi(optarg);
break;
case 't':
tracefile = optarg;
break;
case '?':
printf("未知选项或缺少参数\n");
return -1;
}
}
return 0;
}
首先,我们的cache的定义是这样的cacheline ** cache
,因此要用二维数组的方式对这个cache进行初始化。
先分配出S个行,然后给每行分配出E列
void Init() {
S = 1 << s;
cache = (cache_line **)malloc(S * sizeof(cache_line *));
for (int i = 0; i < S; i++) {
cache[i] = (cache_line *)malloc(E * sizeof(cache_line));
// 记得初始化每一行的tag为0
for (int j = 0; j < E; j++) {
cache[i][j].tag = 0;
}
}
}
首先,我们读取进来的tracefile其实只是一个文件名,还需要根据这个文件名去真正的取到这个文件
这就要使用这个了FILE *file_ptr;
int GetOperation() {
// 读取文件,获得访问记录
file_ptr = fopen(tracefile, "r");
if (file_ptr == NULL) {
printf("无法打开文件 %s\n", tracefile);
return -1;
}
char buffer[100];
char command;
int address;
int size;
while (fgets(buffer, sizeof(buffer), file_ptr)) {
if (buffer[0] == 'I') {
continue;
}
if (sscanf(buffer, " %c %x,%d", &command, &address, &size) == 3) {
// 成功获取操作
Func(command, address, size);
// printf("%c %x,%d\n", command, address, size);
} else {
// 输入不合法
return -1;
}
}
fclose(file_ptr);
return 0;
}
申请了多少就释放多少,先释放列,再释放行
void Destory() {
for (int i = 0; i < S; i++) {
free(cache[i]);
}
free(cache);
}
整体来说,这个实验并不难。因为根本没用到什么难的算法,暴力遍历就完事了。
但是因为前前后后所有内容都需要自己实验,还是有不少dirtywork的,这可能比较像真正工作上写的代码,而不是算法题的代码
给我们的cache配置是s = 5, E = 1, b = 5
即有32个set,每个set有一行,即直接映射,每一行可以存储32字节,即每个set可以存储8个整数
首先,我们要做的是尽量减少miss,即让我们的cache尽可能的去命中。
那么如果直接按照一行一行的操作,会有什么问题呢?
void transpose_submit(int M, int N, int A[N][M], int B[M][N]) {
// i和j枚举出了每个8×8的矩阵的左顶点
for (int i = 0; i < 32; i += 8) {
for (int j = 0; j < 32; j += 8) {
// cnti和cntj则分别枚举这个小矩阵的行和列
for (int cnti = 0; cnti < 8; cnti++) {
for(int cntj=0;cntj<8;cntj++){
B[j+cntj][i+cnti]=A[i+cnti][j+cntj];
}
}
}
}
}
为什么呢?因为,A和B之间存在打架的情况。通过打印地址是可以发现A和B的距离很尴尬,使得A和B的任意一个cacheline都是映射到同一个cache。
可以通过csapp第五章介绍的循环展开的方式来优化,我们可以提前把A的这一行的八个int给取出来,这会将它放到寄存器里,然后我们再去修改B,这时候就不会撞车了。具体实现如下。
void transpose_submit(int M, int N, int A[N][M], int B[M][N]) {
// i和j枚举出了每个8×8的矩阵的左顶点
for (int i = 0; i < 32; i += 8) {
for (int j = 0; j < 32; j += 8) {
// cnt枚举的是小矩阵的行数
for(int cnt=0;cnt<8;cnt++){
int temp1 = A[i + cnt][j];
int temp2 = A[i + cnt][j + 1];
int temp3 = A[i + cnt][j + 2];
int temp4 = A[i + cnt][j + 3];
int temp5 = A[i + cnt][j + 4];
int temp6 = A[i + cnt][j + 5];
int temp7 = A[i + cnt][j + 6];
int temp8 = A[i + cnt][j + 7];
B[j][i + cnt] = temp1;
B[j + 1][i + cnt] = temp2;
B[j + 2][i + cnt] = temp3;
B[j + 3][i + cnt] = temp4;
B[j + 4][i + cnt] = temp5;
B[j + 5][i + cnt] = temp6;
B[j + 6][i + cnt] = temp7;
B[j + 7][i + cnt] = temp8;
}
}
}
}
总结来说
64×64就不能直接用8×8了,同样可以先看上面那位大佬的博客画的,使用8乘8的矩阵,自己就和自己冲突了。
而如果使用4×4的,小矩阵内部倒不会冲突,但是因为每次取8个,结果只用了4个,后面还得再取一次,又要碰撞一次。
所以,8×8地访问还是最适合的,因为不会出现二次访问的需求。但是如果直接使用8×8的方式,又会导致自己不断地撞自己。
那综合一下上面的问题,
有一些大佬就提出了一种方法,我们以8×8的小矩阵去取数据,然后将它分成4个4×4的小矩阵
那么如果操作到这里,A的前4个cache行已经全部用完了,后面不会再访问了,并且除去对角线情况,命中率应该是7/8
接下来,我们操作A的左下矩阵,将其放到B的右上矩阵。而B的右上矩阵暂存了A的右上矩阵,这个矩阵应该是放到B的左下矩阵的。所以接下来的这一波操作是关键。
最后一波操作,就是把A的右下矩阵给放到B的右下矩阵中去,这就很轻松了,基本不会miss。
代码实现如下,这个操作说起来还是很抽象的,对着代码看,更加清楚。
void deal_64_64(int M, int N, int A[N][M], int B[M][N]) {
// i和j枚举出了每个8×8的矩阵的左顶点
for (int i = 0; i < 64; i += 8) {
for (int j = 0; j < 64; j += 8) {
int temp1, temp2, temp3, temp4, temp5, temp6, temp7, temp8;
int cnti, cntj;
// 现在处理A的左上4×4,顺便操作一下A的右上4×4
for (cnti = 0; cnti < 4; cnti++) {
// 取出A的左上,以行的方式
temp1 = A[i + cnti][j];
temp2 = A[i + cnti][j + 1];
temp3 = A[i + cnti][j + 2];
temp4 = A[i + cnti][j + 3];
// 取出A的右上,以行的方式
temp5 = A[i + cnti][j + 4];
temp6 = A[i + cnti][j + 5];
temp7 = A[i + cnti][j + 6];
temp8 = A[i + cnti][j + 7];
// 将A的左上放到正确的地方,以列的方式
B[j][i + cnti] = temp1;
B[j + 1][i + cnti] = temp2;
B[j + 2][i + cnti] = temp3;
B[j + 3][i + cnti] = temp4;
// 将A的右上放到现在B的右上,提前处理一下,省的后面还要访问A的这个cacheline
B[j][i + cnti + 4] = temp5;
B[j + 1][i + cnti + 4] = temp6;
B[j + 2][i + cnti + 4] = temp7;
B[j + 3][i + cnti + 4] = temp8;
}
// 至此,A的前4行已经全部处理完了,命中率约为7/8
// 上述操作处理完之后,B的前4行都在cache中
// 现在处理A的左下,将其移到B的右上,同时我们已经把A的右上存在了B的右上
// 所以要记得取下来,放到B的左下去
for (cntj = 0; cntj < 4; cntj++) {
// 取出A的左下,一列一列地取
temp1 = A[i + 4][j + cntj];
temp2 = A[i + 5][j + cntj];
temp3 = A[i + 6][j + cntj];
temp4 = A[i + 7][j + cntj];
// 取出当前B的右上,一行一行地取,之后还要一行一行地放到B的左下
temp5 = B[j + cntj][i + 4];
temp6 = B[j + cntj][i + 5];
temp7 = B[j + cntj][i + 6];
temp8 = B[j + cntj][i + 7];
// 修改B的右上为真正的值,即A的左下
B[j + cntj][i + 4] = temp1;
B[j + cntj][i + 5] = temp2;
B[j + cntj][i + 6] = temp3;
B[j + cntj][i + 7] = temp4;
// 至此B的这一行完全OK了,虽然马上就要被自己干掉,但是也没关系,反正也用不上了
// 接下来别忘了修改B的左下,本来应该是A的右上,但是我们提前存在了B的右上
// 现在就是temp5-8
B[j + cntj + 4][i] = temp5;
B[j + cntj + 4][i + 1] = temp6;
B[j + cntj + 4][i + 2] = temp7;
B[j + cntj + 4][i + 3] = temp8;
}
// 最后修改B的右下
for (cnti = 4; cnti < 8; cnti++) {
temp1 = A[i + cnti][j + 4];
temp2 = A[i + cnti][j + 5];
temp3 = A[i + cnti][j + 6];
temp4 = A[i + cnti][j + 7];
B[j + 4][i + cnti] = temp1;
B[j + 5][i + cnti] = temp2;
B[j + 6][i + cnti] = temp3;
B[j + 7][i + cnti] = temp4;
}
}
}
}
这玩意就很不规则了,之前之所以又是循环展开的,又是小心翼翼控制矩阵大小的,无非是因为
而这一切在61×64的矩阵下都不是问题,因为A或者B内部基本不会碰撞,那矩阵的大小就无所谓了,选一个最小的就行。
#define edge 17
void deal_61_67(int M, int N, int A[N][M], int B[M][N]) {
for (int i = 0; i < N; i += edge) {
for (int j = 0; j < M; j += edge) {
for (int x = i; x < i + edge && x < N; x++) {
for (int y = j; y < j + edge && y < M; y++) {
B[y][x] = A[x][y];
}
}
}
}
}
感谢这位大佬的博客提供的帮助