《深入理解计算机系统》CSAPP_CacheLab


CacheLab

开始日期:21.12.25

操作系统:linux

调试工具:valgrind

目录
  • CacheLab
    • Part A
      • pre-knowledge
      • AC code
      • Test
    • Part B
      • 32 * 32
      • 64 * 64
      • 61 * 67
  • Conclusion

Part A

pre-knowledge

如上图所示,我们需要理清楚organization(组织)和address(地址)的区别
organization表明cache(高速缓存)是如何组织的
address表明指令的位置,当cpu需要执行某指令(instruction)时(instruction被存储在cache组织中),我们通过address找到这个指令。


同第一张图一起理解所给的参数(parameter),大小写不同时所对应的意思也是不同


那么我们可以知道writeup.pdf所给的例子:

linux> ./csim-ref -s 4 -E 1 -b 4 -t traces/yi.
tracehits:4 misses:5 evictions:3

等价于:(s, E, b)中的数值为:2^4 sets, 1 line per set, block of 4 bytes per line
(此处用英文来理解会更好,address的位数默认为32bits

从cache中读取指令,采用的策略是LRU(Least Recently Used),该策略可以看作一个算法,简单来说,该算法是将最近最少使用的line重置,实现可以用累加器(时间戳)和队列,这里使用的是累加器(counter),具体代码实现见下文。

AC code

  • 明确这只是个简易模拟器,并不需要完全模拟

  • 首先是使用到的库,主要使用的函数或者数据类型见注释

  • 友情链接:以及cmu给的更多提示

    #include "cachelab.h" //printSummary()
    #include //fopen() fclose() fscanf() 
    #include //getopt()
    #include  //malloc()
    #include  //strcpy()
    #include  //bool
    
  • 然后是数据结构和一个结构体全局变量

    • 参数Args是服务于linux指令的(eg. ./csim-ref -s 4 -E 1 -b 4 -t traces/yi.
    • Line, Set, Sets是用来构建Cacha的(在之后的函数部分会给出如何构建的简易图示)
    • Result是我们最终输出结果,用结构体表示能节省空间
    typedef struct {
        bool h;
        bool v;
        int s;
        int E;
        int b;
        char t[50];
    }Args;
    
    typedef struct{
        int miss;
        int hit;
        int eviction; 
    }Result;
    
    typedef struct{
        bool valid;
        int tag;
        int counter; // use LRU by countering numbers of valid
    }Line, *Set, **Sets; // set == one ground of lines, all sets == one cache
    
    Result result;
    
    
  • 函数声明和主函数调用内容

    • 步骤是:先根据linux指令初始化参数,再根据参数构建Cache(sets),然后解析得出结果(解析过程会用到get()函数),最后打印结果
    Args initArgs(int argc, char* const argv[]);
    Sets createCache(Args args);
    void parse(Sets sets, Args args);
    void printSummary(int hits, int misses, int evictions);
    void get(Sets sets, Args args, unsigned int address);
    
    int main(int argc, char* const argv[]){
    	  Args args = initArgs(argc, argv); // initialize arguments from linux command(hvs:E:b:t:)
        Sets sets = createCache(args); // initialize one Cache by arguments
        parse(sets, args);  // parse Cache and get result
        printSummary(result.hit, result.miss, result.eviction); //print result
        return 0;
    }
    
  • 接下来进入函数部分

  • 首先是初始化参数这里用到了getopt函数(友情链接:以及cmu给的更多提示)以及归属于getopt的atol函数,该函数将参数转化为了长整型,还有复制文件名的strcpy函数。

    • -h 和 -v指令是可以无视的,不影响评分,所以这里的具体代码只是服务linux指令,没有作用
    Args initArgs(int argc, char* const argv[]){
        Args args;
        args.h = false;
        args.v = false;
        int opt;
        while (-1 != (opt = getopt(argc, argv, "hvs:E:b:t:"))){ //argues the 'agrs' one by one
            switch (opt){
            case 'h':
                args.h = true; //help_message, but we don't have to write it
                break;
            case 'v':
                args.v = true; //verbose mode(express detials), but we don't have to write it
                break;
            case 's':
                args.s = atol(optarg); // change s(int) to long type
                break;
            case 'E':
                args.E = atol(optarg); 
                break;
            case 'b':
                args.b = atol(optarg); 
                break;
            case 't':
                strcpy(args.t, optarg); //copy tracefile'name to optarg 
                break;                                                            
            default:
                args.h = true;
                break;
            }
        }
        return args;
    }
    
  • 根据参数构建Cache(Sets)

    • 一个Cache即是一个Sets,由S个set组成的,而每一个set又是由E个line组成的,采用malloc即可构建
    • 可以看到,一个Sets其实是以结构体line为元素组成的2维矩阵Sets[S][E]
    • 注意到Sets、set分别是二级指针、一级指针(更详细的解释可以看这的图示)
    • valid统一设为false,保证Cache一开始是的,即没有还没有执行任何操作,所有line都是无效的
    • tag设为-1 = 0xffffffff-1 = 0xffffffff或者不设都可以,因为后续都是直接address中的tag给替代掉的
    • counter是统计valid有效次数的,当然一开始也统一为0
    Sets createCache(Args args) {
        int S = 1 << args.s; //S = 2^s
        int E = args.E;
        Sets sets = (Sets)malloc(sizeof(Set) * S); //build Sets(one Sets have S numbers of Set), 2D(row and colum)
        for (int i = 0; i < S; i++) {
            sets[i] = (Set)malloc(sizeof(Line) * E); //build Set(one Set have E numbers of line), [i] is number of row
            for (int j = 0; j < E; j++) { //build Line
                sets[i][j].valid = false;
                sets[i][j].tag;
                sets[i][j].counter = 0;
            }
        }
        return sets;
    }
    
    • 这是一个简易Cache,Sets[4][5]

      // sets[4][5]:
      // Line Line Line Line Line
      // Line Line Line Line Line
      // Line Line Line Line Line
      // Line Line Line Line Line
      
  • 解析得出结果

  • 首先是parse函数

    • 涉及到了fopen() fclose() fscanf() ,作用分别是打开,关闭,读入文件(链接:fopen)
    • 这里读入的文件指令显然和linux指令不同,是从tracefile中读取的,给出了操作,地址和字节大小
    • I,载入指令,在这个简易的cache simulator中没有对应的实际操作
    • 记得要释放内存
    void parse(Sets sets, Args args) {
        FILE* fp = fopen(args.t, "r"); //open a tracefile with 'r'ead mode
        if (fp == NULL) {
            printf("open error");
            exit(0); //exit this process
        }
        
        char operation;
        unsigned int address;
        int size;
    
        while(fscanf(fp, " %c %xu,%d", &operation, &address, &size) > 0) { //write instrution from file, abandon 'I' without [space]
            switch(operation) {
                // we don't care 'I'
                case 'L': 
                    get(sets, args, address);
                    break;
                case 'M': // 'M' equal two get() so we don't need 'break;'
                    get(sets, args, address);
                case 'S':
                    get(sets, args, address);
                default:;
            }
        }
        fclose(fp);
    
        // free store
        for (int i = 0, S = 1 << args.s; i < S;i++) {
            free(sets[i]);
        }
        free(sets);
    }
    
  • 最后是get函数

    • 把一个文件指令给出的地址转化为我们要的index(组号)和tag,这个简易不需要用到data of block及block offset(具体转化过程见注释)

    • 然后根据index(组号)和tag先找出对应的set(组),注意要和sets集合区别开来

    • 一个set由许多line组成,一个line的构成见下:

      //line ==> 0x|valid bit|tag bits|counter bits|
      
    • 这里我用set = sets[index]表示哪一组,用set[j]表示这一组哪一行

    • LRU的具体实现依靠counter(累加器),在只要valid有效counter就加一的操作之后,所有情况见下:

    • valid tag operation
      false unequal miss++,get tag,valid = 1,counter = 0
      false equal miss++,get tag,valid = 1,counter = 0
      true unequal eviction++,miss++,get tag,counter = 0
      true equal hit++,counter = 0
    • 这里需要特别注意的是,当valid有效且tag不相等时,我们就要找到目前该组counter最大的line舍去它,同时将其重置

    • 在这里,所谓的LRU,最近最少使用,counter最大就说明这个line离寄存器最近且最少使用

    • 在c语言中,无论是有符号数还是无符号数,逻辑/算术左移统一补0逻辑右移也统一补0;但只有一种特殊情况:有符号数执行算术右移时,补符号位

    void get(Sets sets, Args args, unsigned int address) {
      	// mask ==> 0x|0...|1 of s bits|
        unsigned long mask = 0xffffffffffffffff >> (64 - args.s); 
        //because it is 64bits computer and atol(optarg)
        
        // get index of set
      	//address = 0x|t bits|s bits|b bits| ==> address = 0x|0...|t bits|s bits|
        address >>= args.b; 
        int index = mask & address; //index = 0x|0...|s bits| 
        int tag = address >> args.s; //tag = 0x|0...|t bits|
        Set set = sets[index]; 
    
        //LRU
        int j;
        int E = args.E;
    
        //counter == valid_counter
        for (j = 0; j < E; j++) {
            if (set[j].valid == true) {
                set[j].counter++; 
            }
        }
    
        //valid and equaling of tag so hit++
        for (j = 0; j < E; j++) {
            if (set[j].valid == true && set[j].tag == tag) {
                set[j].counter = 0;
                result.hit++;
                return;
            }
        }
    
        //invalid so miss++, change to valid and get tag bits(tag = 0x|0...|t bits|)
        for (j = 0; j < E; j++) {
            if (set[j].valid == false) {
                result.miss++;
                set[j].valid = true;
                set[j].tag = tag;
                return;
            }
        }
    
        //no equaling of tag so eviction++, evict line of max_counter(change to 0) and update tag
        //in the meantime, miss++
        result.eviction++;
        result.miss++;
        int max_counter = set[0].counter;
        int max_j = 0;
        for (j = 1; j < E; j++) {
            if (set[j].valid == true && set[j].counter > max_counter) {
                max_j = j;
                max_counter = set[j].counter;
            }
        }
        set[max_j].tag = tag;
        set[max_j].counter = 0;
        return;   
    }
    

Test

(注意csim.c cachelab.c要link起来)



完成日期:21.12.28

更新:21.12.30:参数构建Cache部分,增加了一些注释;增加测试结果图片

参考链接:

《深入理解计算机系统》配套实验4: Cache lab
CSAPP实验之cachelab

ps:最近在练习写英文注释,有错误的话,敬请指正

Part B

开始日期:22.1.8

请注意要安装valgrind

valgrind --version # 查看版本号检查
sudo -i # 进入root模式后才能安装
apt install valgrind # 安装

以及要连续完成这三条linux指令之后才能执行./test-trans,一直运行不了函数,试了好久才发现的,需要这些准备的原因应该是:trace文件中原本存在着需要当作输入的参数linux指令、 过程中还需要用到valgrind,但我们完全不需要知道内部细节。

gcc -O0 -c trans.c #make tran.o
gcc -O0 -o tracegen tracegen.c trans.o cachelab.c #make tracegen.exe
gcc -o test-trans test-trans.c cachelab.c trans.o #make test-trans.exe
./test-trans -M 32 -N 32 #evaluation

这些指令都存放在已经给出的Makefile这个文件当中。

实际上运用:
The program is structured so that it loads a chunk into the L1 cache, does all the reads and writes that it needs to on that chunk, then discards the chunk, loads in the next chunk, and so on.
理解:把数据块再分为块(chunck),每次只读、写一块需要的,然后就丢弃,以此类推

这个lab要用到的:
Blocking: divide matrix into sub-matrices.
Size of sub-matrix depends on cache block size, cache size, input matrix size.
把矩阵分为多个子矩阵
子矩阵大小根据cache block、cache、input 矩阵的大小决定

由此得知道,需要用到blocking技术。

需要注意的是,writeup中的Programming Rules for Part B有给出规则,比较重要的是:

  • 最多使用12个变量
  • 不能使用long类型、bit技巧
  • 不能使用迭代(recoursion)
  • 矩阵A不能修改,矩阵B可以任意修改
  • 不能使用malloc()函数

32 * 32

根据所给参数 -s 5 -E 1 -b 5 ,可以知道,-S 32 -E 1 -B 32 ,从而有下表:(1 int = 4 bytes

line(cache block) 32 (bytes) == 8 (int)
cache(sets) 32 * 8 (int)
A[M][N] 32 * 32 (int)

在给出的提示及waside-blocking得知,blocking(分块技术)应当按照cache block(数据块)来划分,所以blocking为8 * 8的子矩阵。然后根据置换矩阵的要求,对换即可,从而有:
(分块写入比元素写入快了许多,因为元素都是一个个放入,每一个都会cool miss[冷失效],但分块不会,每一个分块只有第一个元素会cool miss)

    if (M == 32){
        int i, j, i1, j1;
        for (i = 0; i < N; i += 8) { //blocking to 8*8 submatrix
            for (j = 0; j < M; j += 8) {
                for (i1 = i; i1 < i+8; i1++) //swap
                    for (j1 = j; j1 < j+8; j1++)
                        B[i1][j1] = A[i1][j1];
            }
        }          
    }

相比最原始的二重循环来置换矩阵,速度已经快了不少:

Since your transpose function is being evaluated on a direct-mapped cache, conflict misses are apotential problem. Think about the potential for conflict misses in your code, especially along the diagonal. Try to think of access patterns that will decrease the number of these conflict misses.
理解:由于这是直接映射cache,那么,使用矩阵A通过cache执行转置函数写到矩阵B的过程中,容易出现冲突失效,同时暗示很有可能是因为对角线的元素。

没有小于300,达不到满分,说明其中出现了问题,很容易想到对角线处的元素的对换过程会出现冲突。

由于这是直接映射cache,初始化完A、 B矩阵之后,那么A、B矩阵相同索引的地址将会映射到相同的cache地址当中

对换实际上是cache对A矩阵执行一次Load操作和对B矩阵一次Store操作,用对换是为了形象描述操作。
转置时,对非对角线的元素执行对换时,因为映射到cache的地址不同所以不会出现冲突,但对角线的元素执行对换时,因为映射到cache的地址相同所以会出现冲突。(映射到cache的地址相同所引起的miss称为conflict miss

如何避免冲突呢?我们按行看,因为cache是按行存放的,一个cache line有8 int,而我们至多有12个变量可以用,我们取8个变量临时存放数据,用空间换取时间上的效率。用8个不可能与A、B矩阵相同的地址,用来暂存数据,那么对角线就不会出现地址位置上的冲突了。代码如下:

if (M == 32){
    int i, j, k, v0, v1, v2, v3, v4, v5, v6, v7;
    for (i = 0; i < N; i += 8) {
        for (j = 0; j < M; j += 8) {
            for(k = i; k < i+8; k++){
                v0 = A[k][j];
                v1 = A[k][j+1];
                v2 = A[k][j+2];
                v3 = A[k][j+3];
                v4 = A[k][j+4];
                v5 = A[k][j+5];
                v6 = A[k][j+6];
                v7 = A[k][j+7];
                B[j][k] = v0;
                B[j+1][k] = v1;
                B[j+2][k] = v2;
                B[j+3][k] = v3;
                B[j+4][k] = v4;
                B[j+5][k] = v5;
                B[j+6][k] = v6;
                B[j+7][k] = v7;
            }
        }
    }          
}

miss = 287,已经满分了,但还有逼近理论miss = 256的解法,祝参考得开心 = / =

64 * 64

比较难,这里主要参考了这篇blog1

一般分块是按照block的规模来blocking的,但在这道题中,最好按照矩阵在cache中存储规模来决定,32*32时,cache是32*8,也就是说,cache能存8行32个int元素。如果不考虑转置功能,那么每8行之后映射到相同cache位置的元素就会冲突,即B[i][j+8k] = A[i][j]时会出现冲突,之后只能进行替换,那么必然引起两次miss(冲突一次,替换一次)。如果考虑转置功能,那么,只有位于矩阵对角线的元素才会出现位置相同的情况,非对角线的元素对换时永远是B[j][i] = A[i][j] 的情况,不可能是B[i][j+8k] = A[i][j] 。综上,32*32时,我们采用8*8的blocking,因为每8行之后映射到相同cache位置的元素就会冲突

64*64时,cache还是32*8,可以看作cache是64*4,从而cache能存4行64个int元素。那么每4行之后映射到相同cache位置的元素就会冲突,故而我们采用4*4的blocking。

但是4*4并不能合理利用cache的矩阵格式,我们把cache看作是64*4的矩阵,但实际上它是32*8,为了合理利用cache的矩阵格式,我们运用分而治之的思想,仍采用8*8的blocking但处理时把它看作是4个4*4block。

而采用的图示为这篇blog2,主要解决了两个问题才能优化成功达到满分。(下列的abcd,a'b'c'd'都源于blog2

  • 每4行,cache行就会被覆盖,所以写入b'的时候需要再重新写一遍,将导致miss,所以我们考虑现将b'放到原c'的位置,也就是先把b'和c'互换位置,之后再交换回来。
  • 想办法在存入c'的时候直接将b'复原, 这里合理利用了cache:最近使用的cache line先保留下来,以便使用。
    • 将b逆序存放而后在存入c'的时候直接将b'复原
    • 为什么这样可行?因为此时cache中存放有全部的b'的数据,如果数据不是逆序存放,c'、 d'第一行的缓存会覆盖掉b'第一行的缓存,此时无法读入b'第一行放到c'第一行,将会出现miss。反之,如果数据是逆序存放的,因为只覆盖了b'第一行的缓存,但我们读入的是b'第4行的数据,而b'第4行的数据仍然在缓存中,这样就能hit了。
    • 给个灵魂图示:(doge

代码如下:

if (M == 64){
    int i, j, k, h, v0, v1, v2, v3, v4, v5, v6, v7;
    for (i = 0; i < N; i += 8) {
        for (j = 0; j < M; j += 8) {
            for (k = i; k < i + 4; k++) {
                // 8*8,但之后处理时看作4*4
                v0 = A[k][j];
                v1 = A[k][j+1];
                v2 = A[k][j+2];
                v3 = A[k][j+3];
                v4 = A[k][j+4];
                v5 = A[k][j+5];
                v6 = A[k][j+6];
                v7 = A[k][j+7];

                // a
                B[j][k] = v0;
                B[j+1][k] = v1;
                B[j+2][k] = v2;
                B[j+3][k] = v3;

                // b 逆序放到c'(右上角)
                B[j+0][k+4] = v7;
                B[j+1][k+4] = v6;
                B[j+2][k+4] = v5;
                B[j+3][k+4] = v4;
            }
            for (int h = 0; h < 4; h++) {
                // c
                v0 = A[i+4][j+3-h];
                v1 = A[i+5][j+3-h];
                v2 = A[i+6][j+3-h];
                v3 = A[i+7][j+3-h];
                // d
                v4 = A[i+4][j+4+h];
                v5 = A[i+5][j+4+h];
                v6 = A[i+6][j+4+h];
                v7 = A[i+7][j+4+h];

                // b'到原本左下角的位置
                B[j+4+h][i+0] = B[j+3-h][i+4];
                B[j+4+h][i+1] = B[j+3-h][i+5];
                B[j+4+h][i+2] = B[j+3-h][i+6];
                B[j+4+h][i+3] = B[j+3-h][i+7];

                // 存放c到原本c'的位置(右上角)
                B[j+3-h][i+4] = v0;
                B[j+3-h][i+5] = v1;
                B[j+3-h][i+6] = v2;
                B[j+3-h][i+7] = v3;
                // 存放d到d'
                B[j+4+h][i+4] = v4;
                B[j+4+h][i+5] = v5;
                B[j+4+h][i+6] = v6;
                B[j+4+h][i+7] = v7;
            }
        }
    }
}

结果:

61 * 67

由于是不规则矩阵,我们的思路也很简单,分而治之即可,分为规则部分、不规则部分,分开处理就行了。
规则部分我们取48*48的矩阵,如果合理利用32*8cache矩阵的话,最好blocking为16*16的矩阵然后再划分为4个8*8处理,但是题目要求很宽松,miss小于2000即可。那满足优化要求分为16*16就行了,参照第一关,处理对角线元素即可。而不规则部分,只要考虑索引相同的情况即可,因为此时映射到cache的位置相同,例如:B[55][55] = A[55][55]

代码如下:

else{
    int i, j, k, h, v0, v1, v2, v3, v4, v5, v6, v7;
    // 48 * 48,只有8个变量,分开处理
    for (i = 0; i+16 < N; i += 16) {
        for (j = 0; j+16 < M; j+=16) {
            for (k = i; k < i+16; k++) {
                v0 = A[k][j];
                v1 = A[k][j+1];
                v2 = A[k][j+2];
                v3 = A[k][j+3];
                v4 = A[k][j+4];
                v5 = A[k][j+5];
                v6 = A[k][j+6];
                v7 = A[k][j+7];

                B[j][k] = v0;
                B[j+1][k] = v1;
                B[j+2][k] = v2;
                B[j+3][k] = v3;
                B[j+4][k] = v4;
                B[j+5][k] = v5;
                B[j+6][k] = v6;
                B[j+7][k] = v7;

                v0 = A[k][j+8];
                v1 = A[k][j+9];
                v2 = A[k][j+10];
                v3 = A[k][j+11];
                v4 = A[k][j+12];
                v5 = A[k][j+13];
                v6 = A[k][j+14];
                v7 = A[k][j+15];

                B[j+8][k] = v0;
                B[j+9][k] = v1;
                B[j+10][k] = v2;
                B[j+11][k] = v3;
                B[j+12][k] = v4;
                B[j+13][k] = v5;
                B[j+14][k] = v6;
                B[j+15][k] = v7;

            }
        }
    }

    // 处理不规则的部分,分两部分
    for (k = i; k < N; k++) { // k = (i == 48) < 61 , h = 0 < 67
        for (h = 0; h < M; h++){
            B[h][k] = A[k][h];
        }
    }
    for (k = 0; k < i; k++) { // k = 0 < 48, h = (j == 48) < 67
        for (h = j; h < M; h++) {
            B[h][k] = A[k][h];
        }
    }
}

给个图示:

结果:

完成日期:22.1.13

Conclusion

  • 期间因为考试周(21.12.31 ~ 22.1.7)和休息(22.1.9 ~ 22.1.12)暂停了几天。
  • 参考了非常多的内容,写得不容易,写完还是很开心的
  • 后续的lab会继续,但算法和c++的学习要提上日程了
  • 我觉得自己从镜子里走出来了。——《一场事先张扬的凶杀案》马尔克斯