根据高速缓存的原理,我们编写一定的数据结构和算法,来达到计算这些操作的命中(hit),不命中(miss)和牺牲或者赶出(eviction)
根据题意和网络上的操作资料,我们可以整理出以下的大致思路:
1.我们的在main函数中,必须能够读取相关参数的输入,处理各种输入参数和错误情况
a)使用while循环和switch语句的结合来搞事
2.我们必须组织一定的数据结构和算法来模拟实现高速缓存的原理
3.在逐行读取文件数据的同时,完成算法(判断是否命中,如果不命中需要重新加载块,并且把原来的块写回内存),统计相关的数据。
-h get help info
-v Optional verbose flag that displays trace info 可选的详细标志,可以显示trace信息
-s <s> Number of set index bits 设置索引位的数量,即设定了组数
-E <E> Associativity (number of lines per set) 设置每组的行数
-b <b> Number of block bits 设定了块大小
-t <tracefile>: Name of the valgrind trace to replay 设定trace文件
再次值得注意的是,这里题目是让我们完成一个模拟器并不是在数据结构中真正存储数据,所以我们对于每一行只需要有三个数据域即可:
1.有效位
2.标记位
3.时间戳(用于LRU策略)
然后我们使用一个该结构的二维数组即可表示整个高速缓冲的每个行的状态,然后也就是能判断是否命中等等操作
具体为cache[S][E] S代表共计有几组,E表示一组内有几行。参考书中话表格的表达方式。
思路逻辑大概有了,开始写。
getopt函数的典型用法参考了cmu的教程:
http://www.gnu.org/software/libc/manual/html_node/Example-of-Getopt.html#Example-of-Getopt
fscanf函数的例子和用法参考cmu的教程:
http://crasseux.com/books/ctutorial/fscanf.html
深刻的经验教训:(以后绝对不偷懒了,一定写一个测试一个,一步一步脚印,相处,边测试边推进,远远比写一大段,最后出现莫名奇妙的错误好。尤其是c没有边界检查。)
1. 务必好好领会下值传递和引用传递的区别。搞清楚,啥时候需要值传递,什么时候需要引用传递。
2. 第二点,务必写一个函数,测试检查一个函数。起码要打印出出来,尤其是使用malloc的时候。
3. ide工具真的很重要,这是我第一次在linux上记事本 + gdb调试,真的是,我选择vs。vs真好用。
#include "cachelab.h"
#include <getopt.h>
#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
/*
今天就是要读取一个输入文件,根据输入文件的操作符,操作数,大小。
根据高速缓存的原理,我们编写一定的数据结构和算法,来达到计算这些操作的命中(hit),不命中(miss)和牺牲或者赶出(eviction)
根据题意和网络上的操作资料,我们可以整理出以下的大致思路:
1.我们的在main函数中,必须能够读取相关参数的输入,处理各种输入参数和错误情况
a)使用while循环和switch语句的结合来搞事
2.我们必须组织一定的数据结构和算法来模拟实现高速缓存的原理
3.在逐行读取文件数据的同时,完成算法(判断是否命中,如果不命中需要重新加载块,并且把原来的块写回内存),统计相关的数据。
-h get help info
-v Optional verbose flag that displays trace info 可选的详细标志,可以显示trace信息
-s <s> Number of set index bits 设置索引位的数量,即设定了组数
-E <E> Associativity (number of lines per set) 设置每组的行数
-b <b> Number of block bits 设定了块大小
-t <tracefile>: Name of the valgrind trace to replay 设定trace文件
再次值得注意的是,这里题目是让我们完成一个模拟器并不是在数据结构中真正存储数据,所以我们对于每一行只需要有三个数据域即可:
1.有效位
2.标记位
3.时间戳(用于LRU策略)
然后我们使用一个该结构的二维数组即可表示整个高速缓冲的每个行的状态,然后也就是能判断是否命中等等操作
具体为cache[S][E] S代表共计有几组,E表示一组内有几行。参考书中话表格的表达方式。
思路逻辑大概有了,开始写。
getopt函数的典型用法参考了cmu的教程:
http://www.gnu.org/software/libc/manual/html_node/Example-of-Getopt.html#Example-of-Getopt
fscanf函数的例子和用法参考cmu的教程:
http://crasseux.com/books/ctutorial/fscanf.html
深刻的经验教训:(以后绝对不偷懒了,一定写一个测试一个,一步一步脚印,相处,边测试边推进,远远比写一大段,最后出现莫名奇妙的错误好。尤其是c没有边界检查。)
1. 务必好好领会下值传递和引用传递的区别。搞清楚,啥时候需要值传递,什么时候需要引用传递。
2. 第二点,务必写一个函数,测试检查一个函数。起码要打印出出来,尤其是使用malloc的时候。
3. ide工具真的很重要,这是我第一次在linux上记事本 + gdb调试,真的是,我选择vs。vs真好用。
*/
//定义模拟高速缓存的每一行:即有效位,标记位,以及高速缓存的块大小
typedef struct{
int valid;//有效位的标志
long tag;//标记位
long time_tamp;//时间戳,记录当前行的存入时间
}cache_line;
typedef cache_line* cache;
/*
main函数,主要处理相关参数的处理
主要是学习getopt函数的使用,看网络上的资料就是结合while和switch语句来实现每个参数的跳转。
*/
static void printHelpInfo();
static void get_opt_xl(int argc, char** argv, char* optStr, int* ep, int* sp, int* bp, char** file, int* vp);
static void allocate_all_cache_line(cache* cache_p, int s, int e, int b);
static void read_file_and_excute(cache* cache_p, char* file, int* hitp, int* missp, int* evicp, int e, int s ,int b, int vflag);
static void free_cache(cache* cache_p, int e, int s, int b);
static void handle(cache* cache_p,char op, long addr, int size, int e, int s ,int b, int* hit_p, int* miss_p, int* evic_p, int current_time_tamp, int vflag);
int main(int argc, char** argv)
{
//初始化变量
int hit = 0;
int miss = 0;
int evictions = 0;
char* const optstr = "hvs:E:b:t:";
int e = -1;
int s = -1;
int b = -1;
int vflag = 0;
char* file = NULL;
cache cache = NULL;
//第一部分,设定参数
get_opt_xl(argc, argv, optstr, &e, &s, &b, &file, &vflag);
//printf("s = %d, e = %d, b = %d\n", s, e ,b);
//第二部分:动态分配内存生成高速缓冲行的所需的数据结构,准确地就是生成高速缓冲二维数组
allocate_all_cache_line(&cache, s, e, b);
/*
第三部分:逐行读取文本
a)判断是否操作符:I L M S
b)根据操作符的不同和高速缓冲的原理,判断命中,以及不命中情况下根据LRU策略来驱逐一行,再重新加载数据块,相关的计数器必须计数。
*/
read_file_and_excute(&cache, file, &hit, &miss, &evictions, e ,s ,b, vflag);
//最后部分:free掉molloc出来的堆
free_cache(&cache, e, s, b);
printSummary(hit, miss, evictions);
return 0;
}
//-h选项下打印输出相关的信息
static void printHelpInfo(){
printf("-h get help info\n");
printf("-v Optional verbose flag that displays trace info 可选的详细标志,可以显示trace信息\n");
printf("-s <s> Number of set index bits 设置索引位的数量,即设定了组数\n");
printf("-E <E> Associativity (number of lines per set) 设置每组的行数\n");
printf("-b <b> Number of block bits 设定了块大小\n");
printf("-t <tracefile>: Name of the valgrind trace to replay 设定trace文件\n");
}
//使用getopt函数把参数全部传入进来。
//这里,这里使用了指针来传递,保证修改的同一个值
static void get_opt_xl(int argc, char** argv, char* const optstr, int* ep, int* sp, int* bp, char** file, int* vp){
int c;
opterr = 0;
while((c = getopt(argc, argv, optstr)) != -1){
switch(c){
case 'h':
printHelpInfo();
exit(0);
break;
case 'v':
*vp = 1;
break;
case 's':
//注意这里返回的是字符串,所以需要atoi函数完成转换
*sp = atoi(optarg);
//printf("S = %d\n",*sp);
break;
case 'E':
*ep = atoi(optarg);
//printf("E = %d\n",*ep);
break;
case 'b':
*bp = atoi(optarg);
//printf("b = %d\n",*bp);
break;
case 't':
*file = optarg;
//printf("file:%s\n", *file);
break;
case '?':
printf("未知参数");
printHelpInfo();
exit(0);
default:
abort();
}
}
}
//使用malloc函数动态分配内存
static void allocate_all_cache_line(cache* cache_p, int s, int e, int b) {
//首先分配出s组出来,每个组也就是
// bug 1 索引位数应该换算成实际组数
int bigs = 1 << s;
*cache_p = (cache_line*)(malloc(sizeof(cache_line) * bigs * e));
cache cache = *cache_p;
for(int i = 0; i < bigs * e;++i){
cache[i].valid = 0;
cache[i].tag = 0;
cache[i].time_tamp = 0;
}
}
//逐行读取文件,根据高速缓冲原理,完成计数
static void read_file_and_excute(cache* cache_p, char* file, int* hitp, int* missp, int* evicp, int e, int s ,int b, int vflag){
//初始化变量
cache cache = *cache_p;
char op;
long addr;
int size;
FILE* my_stream;
//第一部分,使用io函数,打开文件
my_stream = fopen(file, "r");
if(my_stream == NULL){
printf("找不到文件:%s\n",file);
fflush(stdout);
exit(0);
}
long cnt = 1;
//fscanf函数返回读到文件结尾会返回null
while(!feof(my_stream)){
int tr = fscanf(my_stream, "%c %lx,%x", &op, &addr, &size);
if(tr != 3){
continue;
}
else{
if(op != 'I'){
if(vflag)
printf("%c %lx,%x ",op,addr,size);
switch(op){
case 'L':
handle(&cache, op, addr, size, e ,s ,b, hitp, missp, evicp, cnt++, vflag);
break;
case 'S':
handle(&cache, op, addr, size, e ,s ,b, hitp, missp, evicp, cnt++, vflag);
break;
case 'M':
//need twice
handle(&cache, op, addr, size, e ,s ,b, hitp, missp, evicp, cnt++, vflag);
handle(&cache, op, addr, size, e ,s ,b, hitp, missp, evicp, cnt++, vflag);
break;
}
if(vflag){
printf("\n");
fflush(stdout);
}
}
}
}
fclose(my_stream);
}
//释放分配的内存空间
static void free_cache(cache* cache_p, int e, int s, int b){
if(*cache_p == NULL)
return;
free(*cache_p);
}
/*
判断是否命中,根据高速缓冲原理,首先,我们提取出组索引,然后在组内寻找符合的标记位,同时检查它的有效位
*/
static void handle(cache* cache_p,char op, long addr, int size, int e, int s ,int b, int* hit_p, int* miss_p, int* evic_p, int current_time_tamp, int vflag){
//首先,先新建一个局部变量来引用cache
cache cache = *cache_p;
//解析下addr,分离出组索引,标记位
//也就是前面学习的位级操作
long co = ((0x1 << b) - 1) & addr;//块偏移
long ci = ((0x1 << s) - 1 ) & (addr >> b);//组索引
long ct = ((~((0x1 << (b + s)) - 1)) & addr) >> (b + s);//标记位
//test
printf("ct:%lx\tci:%lx\tco:%lx\t\n",ct, ci, co);
//将co,ci,ct转换成数组索引的形式
int sindex = ci * e;
int i;
//在组内遍历寻找有对应标记的缓存行
for(i = sindex;i < sindex + e; i++){
if(cache[i].tag == ct && cache[i].valid == 1){
break;
}
}
//根据索引判断是否命中,并且分情况处理
if(i != sindex + e){
//命中情况
//题意中指出不会出现这种情况,保险起见我还是加上了
if(co + size > (1 << b)){
printf("这里!");
}
//judge size
++(*hit_p);
if(vflag)
printf("hit ");
}
else{
//不命中
if(vflag)
printf("miss ");
++(*miss_p);
cache_line* empty_line;
cache_line* replace_line;
//寻找有无空行
for(i = sindex;i < sindex + e; i++){
if(cache[i].valid == 0){
empty_line = &(cache[i]);
break;
}
}
//判断是否有空行
if(i != sindex + e){
replace_line = empty_line;
}
else{
for(replace_line = &(cache[sindex]),i = sindex;i < sindex + e;++i){
if(cache[i].time_tamp < (replace_line->time_tamp)){
replace_line = &(cache[i]);
}
}
++(*evic_p);
if(vflag)
printf("eviction ");
}
replace_line->valid = 1;
replace_line->tag = ct;
replace_line->time_tamp = current_time_tamp;
}
//test
printf("cnt:%d\n",current_time_tamp);
for(int j = 0;j < e*(1 << s); ++j){
printf("%d",j);
printf("\tvalid:%d \ttag:%lx \ttime_tamp:%ld\n",cache[j].valid, cache[j].tag, cache[j].time_tamp);
}
//printf("\thandle return!");
}
这里我运行的结果,非常蛋疼。可以看到,我两个的结果不对,我查了半天的bug,硬是没找到,后期再review重构下吧,我估计就是,某个不起眼的地方出错了。还是那句话,以后一定要写一个函数,测试下,边测试,边推进,否则到了最后找这种小bug,真的很痛苦。
---------------分割线----------------------------
这里我自己写了3个版本,想了很久才想到8 * 8分块,后面64 * 64 更进一步的4 * 4分块没想到,借鉴网上大佬的思路。但是,我的理解还是不是很到位,无法满分,后面再review想下,感谢网上贡献思路的大佬。
我的最终版本,我实在想不出来,怎么改进了。32 * 32 利用八个局部变量,可以完成最大限度的局部性,
61*67由于错开了,不是2的幂次,所以反而来说,只要我充分使用一次读取八个数的优势,使用八个变量缓存下就能最大限度的发挥局部性。
而64 * 64 是真的,由于特殊性,它的第一行,和第五行,直接冲突了,导致我们只用 4* 4 的分块处理。
这里的办法,参考了http://wdxtub.com/2016/04/16/thick-csapp-lab-4/的思路。感谢。
甚至于,到了128 * 128第一行,会和第三行冲突,就麻烦了。
所以,除了这里的特殊办法,更多是,在两个数组基地址上,插入一个空白段,这样错开缓冲映射,不要动不动就冲突了。
char transpose_submit_desc[] = "Transpose submission";
void transpose_submit(int M, int N, int A[N][M], int B[M][N])
{
int i, j, temp1, temp2, temp3, temp4, temp5, temp6, temp7, temp8, pp ,qq;
//int n = N / 8 * 8;
//int m = M / 8 * 8;//先用8 * 8 处理大的块
//外层两个控制两个分块矩阵的首元素的变化
if(M == 64 && N == 64){
//外层两个控制两个分块矩阵的首元素的变化
for(i = 0;i < N / 8 * 8;i += 8){//行
for(j = 0;j < M / 8 * 8; j += 8){//列
//在对角巷内部再使用局部变量来优化,我们使用4 * 4分块细化8 * 8
// 4*4分块内部,一行4个的转移。
//左上角,
qq = 0;
for(pp = 0;pp < 4;pp++){
temp1 = A[i + pp][j + qq];
temp2 = A[i + pp][j + qq + 1];
temp3 = A[i + pp][j + qq + 2];
temp4 = A[i + pp][j + qq + 3];
B[j + qq][i + pp] = temp1;
B[j + qq + 1][i + pp] = temp2;
B[j + qq + 2][i + pp] = temp3;
B[j + qq + 3][i + pp] = temp4;
}
//右上角和左上角,我觉得还可以改进下,每
//右上角
qq = 4;
for(pp = 0;pp < 4;pp++){
temp1 = A[i + pp][j + qq];
temp2 = A[i + pp][j + qq + 1];
temp3 = A[i + pp][j + qq + 2];
temp4 = A[i + pp][j + qq + 3];
B[j + qq][i + pp] = temp1;
B[j + qq + 1][i + pp] = temp2;
B[j + qq + 2][i + pp] = temp3;
B[j + qq + 3][i + pp] = temp4;
}
//右下角
for(pp = 4;pp < 8;pp++){
temp1 = A[i + pp][j + qq];
temp2 = A[i + pp][j + qq + 1];
temp3 = A[i + pp][j + qq + 2];
temp4 = A[i + pp][j + qq + 3];
B[j + qq][i + pp] = temp1;
B[j + qq + 1][i + pp] = temp2;
B[j + qq + 2][i + pp] = temp3;
B[j + qq + 3][i + pp] = temp4;
}
//左下角
qq = 0;
for(pp = 4;pp < 8;pp++){
temp1 = A[i + pp][j + qq];
temp2 = A[i + pp][j + qq + 1];
temp3 = A[i + pp][j + qq + 2];
temp4 = A[i + pp][j + qq + 3];
B[j + qq][i + pp] = temp1;
B[j + qq + 1][i + pp] = temp2;
B[j + qq + 2][i + pp] = temp3;
B[j + qq + 3][i + pp] = temp4;
}
}
}
}
else{
for(j = 0;j < M / 8 * 8;j += 8){//列
for(i = 0;i < N / 8 * 8; ++i){//行
temp1 = A[i][j];
temp2 = A[i][j + 1];
temp3 = A[i][j + 2];
temp4 = A[i][j + 3];
temp5 = A[i][j + 4];
temp6 = A[i][j + 5];
temp7 = A[i][j + 6];
temp8 = A[i][j + 7];
B[j][i] = temp1;
B[j + 1][i] = temp2;
B[j + 2][i] = temp3;
B[j + 3][i] = temp4;
B[j + 4][i] = temp5;
B[j + 5][i] = temp6;
B[j + 6][i] = temp7;
B[j + 7][i] = temp8;
}
}
}
//然后处理剩余的部分,右上角
for(i = 0;i < N / 8 * 8;++i){
for(j = M / 8 * 8;j < M;++j){
B[j][i] = A[i][j];
}
}
//左下角
for(i = N / 8 * 8;i < N;++i){
for(j = 0;j < M / 8 * 8;++j){
B[j][i] = A[i][j];
}
}
//右下角
for(i = N / 8 * 8;i < N;++i){
for(j = M / 8 * 8;j < M;++j){
B[j][i] = A[i][j];
}
}
}
64 * 64 的矩阵使用8 * 8分块基础上更近一步的4 * 4分块。(这里我觉得还有改进的空间)
32*32以及61*67,直接使用八个局部变量,使得后面七次读取,必定命中,充分利用了缓冲块中一次载入8个int。
其实,我觉得高速缓冲的核心思想就是如何利用一次读取8个int事实,减少miss。
最后,成绩依然不满分:
多读书,多思考,多画图理解,多编写代码,多测试每个函数。