csapp cachelab_CSAPP实验之cachelab

ee4a4d32118e6b9bfd527b06b4300987.png

有一说一,cache lab有点难。本文参考了网络上不少大神的博文,终于是做到了满分。做这个lab之前或者看本文之前建议在浏览器中点开以下材料链接:

WriteUp,本lab的规则与评分标准。
书本内容及实验,这个ppt其实非常好,回顾了书本上的内容,对实验部分也给予了一定的引导作用。
分块技术,CMU早年的一篇文章,配合Lab食用体验更佳。

本文实验环境 Ubuntu 18.04 LTS 非虚拟机。在实验之前还需要装一下valgrind内存泄露检测工具。

sudo apt install valgrind

安装完成之后如果能够检测到valgrind的版本号即安装成功。

valgrind --version

217d5656c7b90683fed10fbd5801a6f3.png

Part A —— Writing A Cache Simulator

在这个part里需要在 csim.c 里编写一个使用LRU策略的 cache 模拟器,要求最终测试结果要和文件中给出的 csim-ref 文件的结果一样。它里面进行验证的命令存在traces文件夹里,打开一个可以看到其中的样子是这样:

...
I  0040052a,7
 S 7ff000384,4
I  00400531,2
I  00400581,4
 L 7ff000384,4
...

其中注意要求,如果碰到 I 开头的命令,不用理会。代码里直接continue就好了。

LRU在操作系统课程中已经讲过了,即被替换掉的块其最后的访问时间是距离现在最远的,体现在代码里可以设置一个时间戳,完成一次操作然后递增,替换的时候比大小就可以了。

这个模拟器还需要几个函数,getopt 函数和 fscanf 函数在开头链接里的ppt里都有示例用法,拿过来稍微改一下就可以了。

对于这个模拟器来说,需要适应不同的 s E b,所以要用 malloc 函数动态分配。

下面给出代码,注释写在代码旁边。建议从最下 main 函数往上看。

#include "cachelab.h"
#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
#include <limits.h>
#include <getopt.h>
#include <string.h>

int h,v,s,E,b,S; // 这个是我们模拟的参数,为了方便在函数里调用,设置成全局

int hit_count , 
    miss_count , 
    eviction_count;  // 三个在 printSummary 函数中的参数,需要不断更新
	
char t[1000]; // 存 getopt 中选项内容,表示的是验证中需使用的trace文件名

typedef struct{
    int valid_bits;
    int tag;
    int stamp;
}cache_line, *cache_asso, **cache;  // cache 模拟器的结构。由合法位、标记位和时间戳组成

cache _cache_ = NULL;  // 声明一个空的结构体类型二维数组

// 打印 helper 内容的函数,-h 命令使用,内容可自定义
void printUsage()
{
    printf("Usage: ./csim-ref [-hv] -s <num> -E <num> -b <num> -t <file>n"
            "Options:n"
            "  -h         Print this help message.n"
            "  -v         Optional verbose flag.n"
            "  -s <num>   Number of set index bits.n"
            "  -E <num>   Number of lines per set.n"
            "  -b <num>   Number of block offset bits.n"
            "  -t <file>  Trace file.nn"
            "Examples:n"
            "  linux>  ./csim-ref -s 4 -E 1 -b 4 -t traces/yi.tracen"
            "  linux>  ./csim-ref -v -s 8 -E 2 -b 4 -t traces/yi.tracen");
}

// 初始化cache的函数
void init_cache()
{
	//多维数组的开辟要一行行malloc
        _cache_ = (cache)malloc(sizeof(cache_asso) * S); 
	for(int i = 0; i < S; ++i)
	{
		_cache_[i] = (cache_asso)malloc(sizeof(cache_line) * E);
		for(int j = 0; j < E; ++j)
		{
			_cache_[i][j].valid_bits = 0;
			_cache_[i][j].tag = -1;
			_cache_[i][j].stamp = -1;
		}
	}
}



void update(unsigned int address)
{
	// 索引地址位可以用位运算,-1U是最大整数,64是因为我电脑是64位
        int setindex_add = (address >> b) & ((-1U) >> (64 - s));
	int tag_add = address >> (b + s);
	
	int max_stamp = INT_MIN;
	int max_stamp_index = -1;

	for(int i = 0; i < E; ++i) //如果tag相同,就hit,重置时间戳
	{
		if(_cache_[setindex_add][i].tag == tag_add)
		{
			_cache_[setindex_add][i].stamp = 0;
			++hit_count;
			return ;
		}
	}
	
	for(int i = 0; i < E; ++i) // 查看有没有空行
	{
		if(_cache_[setindex_add][i].valid_bits == 0)
		{
			_cache_[setindex_add][i].valid_bits = 1;
			_cache_[setindex_add][i].tag = tag_add;
			_cache_[setindex_add][i].stamp = 0;
			++miss_count;
			return ;
		}
	}
	// 没有空行又没有hit就是要替换了
	++eviction_count;
	++miss_count;
	
	for(int i = 0; i < E; ++i)
	{
		if(_cache_[setindex_add][i].stamp > max_stamp)
		{
			max_stamp = _cache_[setindex_add][i].stamp;
			max_stamp_index = i;
		}
	}
	_cache_[setindex_add][max_stamp_index].tag = tag_add;
	_cache_[setindex_add][max_stamp_index].stamp = 0;
	return ;
}


void update_stamp()
{
	for(int i = 0; i < S; ++i)
		for(int j = 0; j < E; ++j)
			if(_cache_[i][j].valid_bits == 1)
				++_cache_[i][j].stamp;
}


void parse_trace()
{
	FILE* fp = fopen(t, "r"); // 读取文件名
	if(fp == NULL)
	{
		printf("open error");
		exit(-1);
	}
	
	char operation;         // 命令开头的 I L M S
	unsigned int address;   // 地址参数
	int size;               // 大小
	while(fscanf(fp, " %c %xu,%dn", &operation, &address, &size) > 0)
	{
		
		switch(operation)
		{
			//case 'I': continue;	   // 不用写关于 I 的判断也可以
			case 'L':
				update(address);
				break;
			case 'M':
				update(address);  // miss的话还要进行一次storage
			case 'S':
				update(address);
		}
		update_stamp();	//更新时间戳
	}
	
	fclose(fp);
	for(int i = 0; i < S; ++i)
		free(_cache_[i]);
	free(_cache_);            // malloc 完要记得 free 并且关文件
	
}

//===============================================================

int main(int argc, char* argv[])
{
	h = 0; 
	v = 0; 
	hit_count = miss_count = eviction_count = 0;
	int opt; // 接收getopt的返回值
        
        // getopt 第三个参数中,不可省略的选项字符后要跟冒号,这里h和v可省略
	while(-1 != (opt = (getopt(argc, argv, "hvs:E:b:t:"))))
	{
		switch(opt)
		{
			case 'h':
				h = 1;
				printUsage();
				break;
			case 'v':
				v = 1;
				printUsage();
				break;
			case 's':
				s = atoi(optarg);
				break;
			case 'E':
				E = atoi(optarg);
				break;
			case 'b':
				b = atoi(optarg);
				break;
			case 't':
				strcpy(t, optarg);
				break;
			default:
				printUsage();
				break;
		}
	}
	
	if(s<=0 || E<=0 || b<=0 || t==NULL) // 如果选项参数不合格就退出
	        return -1;
	S = 1 << s;                // S=2^s
	
	FILE* fp = fopen(t, "r");
	if(fp == NULL)
	{
		printf("open error");
		exit(-1);
	}
	
	init_cache();  // 初始化cache
	parse_trace(); // 更新最终的三个参数

    printSummary(hit_count, miss_count, eviction_count);
    
    return 0;
}

结果应该如下图

c95cf0c67cb552ff0d403d2314e4b769.png
结果是27分满分

Part B —— Optimizing Matrix Transpose

这个 part 要求我们去完成 trans.c 文件中的 transpose_submit 函数,实现矩阵的转置。有几个要求,一是逻辑上一共只准使用不超过 12 个 int 类型局部变量,二是不能用递归,三是不准改变 A 数组的内容,但能在 B 数组里随便写,四是不能定义新的数组也不能用 malloc 函数开辟空间。最终要使得 cache miss 的次数尽可能少。

上面给的文章和ppt里有关于这个 part 一些提示,建议自己认真看一遍。最终给分由三个测试决定,用 csim-ref 或者 test-trans 测试。

已经给定了 cache 的参数 s = 5,b = 5 ,E = 1。那么 cache 的大小就是 32 组,每组 1 行, 每行可存储 32 字节的数据。ppt 里给出了完成本 part 的提示:分块 (blocking)

32 X 32

第一个测试矩阵大小是 32 x 32 的。我们先来分析一下,一个 int 类型数字是 4 字节,cache 中一行 32 字节,可以放 8 个 int 。先用原来给的示例代码看一下 miss 数量。

if 

b1d1db988daa5cd6ebbe7715f38328b9.png

可以看到 miss 数达到了 1183 个,距离我们要求的 300 个以下满分有着不小的距离。这就要求我们去使用分块技术,因为 cache 一行能放 8 个,所以我们分块最好也用 8 的倍数。在 32 x 32的矩阵中,一行有 32 个 int,即 4 个 cache 行,所以 cache 一共可以存矩阵的 8 行。瞧,正好可以用长宽都为 8 的分块,不会造成冲突。那么再按 8x8 的分块写代码测试一下。

if 

5828b0203c2980872baf068624fb91b3.png

这段代码最后 miss 数量有了惊人的下降,降低到了 343 次,看来分块技术很有效果,但是距离满分还有一些差距。

可以发现,A 和 B 中相同位置的元素是映射在同一 cache line 上的,但是因为我们做转置的话,并不会把 A 中某位置的元素放到 B 中相同的地方,除非在对角线上,因为下标是一样的,此时就会发生原地转置。譬如我们已经把 A 的第四行存进去了,但是当要写 B44 时,发生了冲突,第四行被换成 B 的,然后读 A 的时候又换成了 A 的,就多造成了两次 miss。

这个时候可以使用一个简单的办法,因为除了循环需要的 4 个变量外我们还剩余 8 个自由变量可以用,正好可以存一个 cache line。以空间换时间,把一行一次性读完,减少冲突不命中。代码如下

if

9baad0e8051c00d318d30a82718fd1a2.png

结果到了 287,已经满分了。但是我们还可以继续做下去。根据 PPT 或者是文章里的说法,32 x 32的矩阵利用 8x8 分块,理论上的 miss 应该是

现在是287次,哪里出现了问题?现在 A 是在每对 8 个 int 操作时,会不命中一次,即每个块中有且只有一个元素不被命中,这是无法避免的,而 B 现在是不仅在开头有不命中,在对角线上也有不命中,如果能把 B 中的对角线元素命中的话,就可以达到理论上的 256 次 miss。

处理冲突不命中,就是要处理重复的加载。在访问一个块后,这个被加载过的块因为又被访问使得被覆盖,导致第二次访问这个块的时候不命中。这是因为两次访问的 tag 位并不相同。比如在下标为 22 的不命中,因为第一行的数据转置时会访问到 B 的第二行,随后 A 也访问到第二行,接着 B 又重新访问第二行,这个第二次访问第二行的过程即一次冲突不命中。

要避免它,一个是可以在 A 访问第二行之前不让 B 访问第二行,一个是让 A 访问第二行之后不让 B 访问第二行。后者因为要写入所以不可能,就是必须要先读到 A 才能写到 B。那前者的话,我们原本是直接把所有元素转置到 B 中,但是这样就必定访问 B 的第二行。所以我们换一个思路,能否等 A 的下一行的元素访问完了再转置?

这样的话面临一个问题,我们没有地方存放第一行的 8 个元素了,因为自由变量是有限的。但是我们可以在 B 里随便写啊,因为 B 中第一行在第一次冷不命中后,后面对于该行的访问是一定命中的,因为 A 不再访问第一行了,所以我们可以将 A 的元素暂存在 B 的第一行,等到下一行读到变量里再访问 B 的下一行。代码如下:

if 

a6cd2693403734903973a7efff6647c4.png

最终达到了 259 次 miss,剩下 3 次是函数的开销所造成的。

64 X 64

对 64 x 64 的矩阵来说,每行有 64 个 int,则 cache 只能存矩阵的 4 行了,所以如果使用 8x8 的分块,一定会在写 B 的时候造成冲突,因为映射到了相同的块。看一下 8x8 的分块能达到多少 miss。

else 

65cbf248322160f76d577d76a2e6f84f.png

8 分块的效果很不理想,惊人的四千六百多次。没有办法只能尝试使用 4 分块,虽然可能对 cache 的利用效率不高。

else 

b34904872db0f1cf622f181428b4531e.png

一千七百次,还是达不到满分要求,但是已经缩小了很多。这个地方我自己的想法是对 8 分块再进行 4 分块,但是无奈自己代码功力欠缺,敲着敲着逻辑有点混乱,这里直接贴一下我参考网络上一位大神的代码,他的 4 分块读写的顺序和我想的有些差异,但是大差不差,还有图可以参考。建议各位一定要自己去画图,去看代码里怎么对元素进行读写的,然后自己算 miss 率。

else 

2d32ae21982d9c993d2b3402c4df508d.png

网上有大神优化到 1100 次以内,不知道是怎么做到的。太强了只能膜。

61 X 67

这个相对 64 来说好想一点,因为不是正好的边边相等的矩形,所以不一定要用 8 分块,一个个试下来发现单纯分块的话,17 分块达到了最小的是 1950 次。但是这样很粗糙,我们还是用 8 分块去稍微对对角线做下操作,因为 32 x 32 最小的 miss 的方法和这边是一样的,而且写起来太多了,我们就用最简单的存变量的方式去做。

	else if(M == 61)
	{
		int i, j, v1, v2, v3, v4, v5, v6, v7, v8;
		int n = N / 8 * 8;
		int m = M / 8 * 8;
		for (j = 0; j < m; j += 8)
			for (i = 0; i < n; ++i)
			{
				v1 = A[i][j];
				v2 = A[i][j+1];
				v3 = A[i][j+2];
				v4 = A[i][j+3];
				v5 = A[i][j+4];
				v6 = A[i][j+5];
				v7 = A[i][j+6];
				v8 = A[i][j+7];
				
				B[j][i] = v1;
				B[j+1][i] = v2;
				B[j+2][i] = v3;
				B[j+3][i] = v4;
				B[j+4][i] = v5;
				B[j+5][i] = v6;
				B[j+6][i] = v7;
				B[j+7][i] = v8;
			}
		for (i = n; i < N; ++i)
			for (j = m; j < M; ++j)
			{
				v1 = A[i][j];
				B[j][i] = v1;
			}
		for (i = 0; i < N; ++i)
			for (j = m; j < M; ++j)
			{
				v1 = A[i][j];
				B[j][i] = v1;
			}
		for (i = n; i < N; ++i)
			for (j = 0; j < M; ++j)
			{
				v1 = A[i][j];
				B[j][i] = v1;
			}
	}

24af591a0f2bb398015559718ff2cc9c.png

最终是 1905 次 miss,满分了已经。


cache lab 确实比前面的 lab 难度要提升了不少,也着实发现自己的代码功力还不足。

贴几个我觉得写的很好的博文给大家参考。

孟佬的cachelab,猫神的cache lab,pikahan,李秋豪,特别推荐看一下的网络某巨佬

个人代码github下载

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值