CSAPP | Lab5-Cache Lab 深入解析

2024-05-30 1799阅读

CacheLab

文章目录

  • CacheLab
    • 实验概述
    • 一些前置准备
    • Part A
      • 实验预览
      • 思路
      • 数据结构
      • 创建高速缓存
        • 从命令行中读取参数信息
        • 初始化高速缓存
        • 代码
        • 读入内存操作
          • 指令解析
          • 地址映射
          • 代码
          • 模拟高速缓存
            • 代码
            • 释放内存
              • 代码
              • 主函数
                • 代码
                • Part B
                  • 实验预览
                  • 32×32
                    • 优化——分块8×8
                      • 代码
                      • 优化——分块8×8+使用局部变量
                        • 代码
                        • 64×64
                          • 优化——分块8×8
                          • 优化——分块4×4
                            • 代码
                            • 优化——对8×8的块用4×4的方式处理1.0
                              • 代码
                              • 优化——对8×8的块用4×4的方式处理2.0
                                • 代码
                                • 61×67
                                • 评分

                                  “All problems in computer science can be solved by another level of indirection”

                                  实验概述

                                  实验分为两部分:Part A要求编写一个高速缓存模拟器(csim.c),Part B要求优化矩阵转置函数,最小化高速缓存的不命中的次数(trans.c)

                                  这个实验设计得很好,恰好涵盖了书上第6章的两个核心部分:理解高速缓存的工作方式,以及编写高速缓存友好的代码,同时还应用到了p450页介绍的分块技术。建议先阅读以下的资料,再进行实验

                                  CMU课件,最后有对Cache Lab的一些提示:rec07.pdf (cmu.edu)

                                  分块技术的介绍:waside-blocking.pdf (cmu.edu)

                                  讲解分块技术的博客:[读书笔记]CSAPP:16[VB]高速缓存存储器 - 知乎 (zhihu.com)

                                  一些前置准备

                                  按照实验说明,解压cachelab-handout.tar,创建出一个名为cachelab-handout的目录,这是我们Part A和Part B的工作目录,Part A对应的文件是csim.c,Part B对应的文件是trans.c

                                  进到目录后先输入命令:make clean && make,这将会编译csim.c,trans.c,test-trans.c等文件

                                  Part A

                                  实验预览

                                  Part A要求在csim.c文件中编写一个高速缓存模拟器,traces目录下的5个.trace文件是模拟器要执行的内存操作

                                  CSAPP | Lab5-Cache Lab 深入解析

                                  老师提供了一个可供参考的缓存模拟器:csim-ref,这个模拟器有6个参数:

                                  Usage: ./csim-ref [-hv] -s  -E  -b  -t 
                                      • -h: Optional help flag that prints usage info
                                      • -v: Optional verbose flag that displays trace info
                                      • -s : Number of set index bits (S = 2^s is the number of sets)
                                      • -E : Associativity (number of lines per set)
                                      • -b : Number of block bits (B = 2b is the block size)
                                      • -t : Name of the valgrind trace to replay
                                  

                                  和书上使用的参数一致:s是组索引位数,E是每个组包含的高速缓存行的个数,b是块偏移位数,除此之外,参数v表示要详细输出每次内存执行时,高速缓存的模拟情况(命中、不命中、替换);参数t是用于模拟内存操作的.trace文件名,举个例子:

                                  ./csim-ref -v -s 2 -E 2 -b 3 -t traces/yi.trace
                                  

                                  这表示用csim-ref模拟一个高速缓存,执行traces/yi.trace的内存操作。我们设定这个高速缓存有4个组,每个组有2个高速缓存行,每个高速缓存块有8个字节

                                  CSAPP | Lab5-Cache Lab 深入解析

                                  查看一下yi.trace中的内存操作:

                                   L 10,1
                                   M 20,1
                                   L 22,1
                                   S 18,1
                                   L 110,1
                                   L 210,1
                                   M 12,1
                                  

                                  内存操作的格式为:[space]operation address, size,operation 有 4 种:

                                  • I表示加载指令,表示从内存中读取指令内容
                                  • L 加载数据,表示程序从某个内存地址加载了数据。(缓存命中、不命中、执行替换策略)
                                  • S存储数据,表示程序向某个内存地址存储了数据。(写回、写分配)
                                  • M 修改数据,表示程序对某个内存地址的数据进行了修改。

                                    不需要考虑加载指令I,M指令相当于先进行L再进行S,模拟器要做出的反馈有 3 种:

                                    • hit:命中,表示要操作的数据在对应组的其中一行
                                    • miss:不命中,表示要操作的数据不在对应组的任何一行
                                    • eviction:驱逐,表示要操作的数据的对应组已满,进行了替换操作,这个实验的驱逐策略是LRU,选择最后被访问的时间距现在最远的块(CSAPP p423页)

                                      Part A要做什么: 编写csim.c,这个程序的执行效果要与csim-ref相同,能够模拟一个高速缓存器(参数自定义),执行traces/xx.trace中的内存操作过程。这个模拟器不需要真的存储数据,只是计算traces/xx.trace的内存操作过程中,缓存的命中、不命中、LRU替换的数量,然后将这些参数作为答案,传给printSummary函数

                                      如何检验模拟器的正确性: 首先编译csim.c文件,命令为:gcc -o csim csim.c cachelab.c -lm,这将创建可执行文件csim,也就是你的模拟器

                                      有两种检验方法:

                                      • 使用csim-ref作为参考。设定相同的参数,分别用csim-ref和csim作为模拟器,读取同一个.trace文件的内存操作过程,比对两个模拟器输出的hit、miss、eviction的值
                                      • 使用评分程序test-csim,命令为:./test-csim,这个程序将会使用不同的参数设定你的模拟器,并让模拟器执行目录traces中所有的.trace文件,满分是27

                                        思路

                                        我将整个高速缓存模拟器的实现分为3个部分:

                                        1. 创建高速缓存:这包括从命令行中读取参数信息,初始化缓存
                                        2. 读入内存操作:就是读取xx.trace文件中每行的内容
                                        3. 根据读入的内存操作,模拟高速缓存的行为:核心代码,这包括在高速缓存中查找地址所指示的字,对不命中的处理(是加载到一个空的缓存行还是需要执行LRU替换策略)。每次执行一次缓存(caching),就更新缓存(cache)信息(有效位、标志位、时间戳),同时统计hit、miss、eviction

                                        数据结构

                                        根据CMU课件的提示,我使用二维结构体数组实现cache。结构体定义如下:

                                        typedef struct
                                        {
                                            bool valid; //有效位
                                            unsigned long tag;  //标志位
                                            int timestamp;  //时间戳
                                        }cache_line;    //一个高速缓存行
                                        

                                        定义二维结构体数组为cache_line** cache,这个数组的大小是S×E,cache[i]是高速缓存组i,cache[i][j]是高速缓存组i中的高速缓存行j,cache[i][j]中包含有效位(valid)、标志位(tag)、时间戳(timestamp,记录这个高速缓存行最后被访问的时间),由于不需要存储数据,就没有定义高速缓存块

                                        CSAPP | Lab5-Cache Lab 深入解析

                                        创建高速缓存

                                        从命令行中读取参数信息

                                        由于s、E、 b是由命令行参数设定的,所以要先读取命令行中的参数,可以使用课件中提示的getopt函数,参考getopt(3) - Linux manual page (man7.org)

                                        为了便于访问参数s、E、b,我使用全局变量定义这些参数,简化代码。同时,我使用变量verbose来标识是否需要详细输出缓存过程

                                        这里说一个小坑,getopt函数中,没有参数的选项不带":",如果你写成了v:s:E:b:t,即不带参数的-v有了":",那么getopt就会等待参数的输入。当opt为s时,会将读到的参数optarg给v,而不是后出现的s,这就是说,-v将-s的参数作为自己的参数,-s就直接被跳过了,然后后面就正常读了,所以E、b、t都没问题,只有s为0(花费我1h用gdb来回调试才发现的bug😢,这样的细节很要命)

                                        初始化高速缓存

                                        为结构体数组分配空间: 相应的,二维结构体数组的空间也是动态分配的。根据读入的参数s、E、b,使用malloc分配内存

                                        初始化高速缓存: 将cache[i][j]的有效位(valid)设置为0,表示这是一个空的高速缓存行;时间戳(timestamp)设置为0

                                        代码

                                        实现的代码如下:

                                        cache_line** create_cache(int argc, char** argv)
                                        {
                                            int opt;
                                            while(-1 != (opt = getopt(argc, argv, "vs:E:b:t:")))
                                            {
                                                switch(opt)
                                                {
                                                    case 'v':
                                                        verbose = 1;	//设置verbose为1,表示要详细输出缓存过程
                                                        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:
                                                        break;  //程序健壮性检验,如果不是一个合法的参数,就会退出switch,继续while读取
                                                }
                                            }
                                            //申请内存,创建高速缓存,组数为2^s,每一组高速缓存行总数为E个。即创建一个行为2^s,列为E的二维结构体数组
                                            int row = pow(2, s);
                                            int col = E;
                                            cache_line** cache = (cache_line**)malloc(row*sizeof(cache_line*));
                                            if(cache == NULL)
                                            {
                                                printf("Failed to allocate memory!\n");
                                                exit(1);
                                            }
                                            for(int i=0; i
                                                cache[i] = (cache_line*)malloc(col*sizeof(cache_line));
                                                if(cache[i] == NULL)
                                                {
                                                    printf("Failed to allocate memory!\n");
                                                    exit(1);
                                                }
                                            }
                                            //初始化高速缓存,设置有效位为0,时间戳为0
                                            for(int i=0; i
                                                for(int j=0; j
                                                    cache[i][j].valid = 0;  //有效位 置0
                                                    cache[i][j].timestamp = 0;
                                                }
                                            }
                                            return cache;
                                        }
                                        
                                            FILE *fp = fopen(t, "r");
                                            if(fp == NULL)
                                            {
                                                perror("Error opening file");
                                                exit(1);
                                            }
                                            char operation;
                                            unsigned long addr;
                                            int bytes;
                                            //地址映射的组号和标志位
                                            int set;
                                            unsigned long tag;
                                            while(fscanf(fp, " %c %lx,%d", &operation, &addr, &bytes) == 3)
                                            {
                                                set = (addr>b) & (unsigned long)((1 (b+s);
                                                switch(operation)
                                                {
                                                    case 'L':
                                                    case 'S':
                                                        if(verbose) printf("%c %lx,%d ", operation, addr, bytes);
                                                        cache_simulate(cache, set, tag);
                                                        if(verbose) printf("\n");
                                                        break;
                                                    case 'M':
                                                        if(verbose) printf("%c %lx,%d ", operation, addr, bytes);
                                                        cache_simulate(cache, set, tag);
                                                        cache_simulate(cache, set, tag);
                                                        if(verbose) printf("\n");
                                                        break;
                                                    default:
                                                        break;
                                                }
                                            }
                                        }
                                        

                                        模拟高速缓存

                                        根据从内存操作中由地址映射的组索引set、标记tag,模拟缓存过程

                                        为了简化参数,我使用一个数组result[3]来存放hit、miss、eviction的次数。

                                        为了记录缓存行cache[i][j]最后被执行的时间,即设置时间戳cache[i][j].timestamp,我使用一个全局变量T作为整体的时间。初始 T设置为0,每当进行一次缓存(caching),就要对T加1,这样当需要进行LRU替换时,我们遍历查找这个组,驱逐时间戳最小的缓存行。恰好我们在每一次缓存(caching)后,使用update函数更新缓存(cache)的信息,所以当调用update函数时,就意味着进行了一次缓存(caching),因此可以在update函数中对T加1,更新整体的时间

                                        模拟高速缓存的整体逻辑是:

                                        • 首先在高速缓存组cache[set]中进行行匹配:遍历检查缓存行cache[set][j]的有效位和标记位,以确定地址中的字是否在缓存中。如果找到了一个有效的行cache[set][pos],它的标记与地址中的标记tag相匹配,则缓存命中;若遍历了所有的行都不匹配,则为缓存不命中
                                        • 若缓存命中,则直接调用update函数,更新时间戳:cache[set][pos].timestamp = T,并统计hit:result[HIT]++(这里我使用了枚举)
                                        • 若缓存不命中,则需要判断这个组cache[set]是否满了,一种方法是维护一个数组occupancy,它记录了各个组中有效位为1的有效行个数,初始置为0。比较occupancy[set](有效行个数)与E(行总数),判断缓存组cache[set]中是否还有空行
                                          • 若occupancy[set]
                                          • 若occupancy[set] == E,则没有空行,需要执行LRU替换策略。调用函数LRU_replace,遍历整个组cache[set],找到时间戳最小的缓存行,返回这个缓存行的位置pos,然后调用update函数,更新cache[set][pos](同上),并统计eviction:result[eviction]++
                                            代码
                                            //核心代码
                                            void cache_simulate(cache_line** cache, int set, unsigned long tag)
                                            {
                                                bool find = false;		//标识是否命中
                                                int col = E;
                                                int pos = 0;
                                                for(int j=0; j
                                                    //命中了(有效位为1,且与某一个高速缓存行的标志位匹配上了)
                                                    if(cache[set][j].valid == 1 && cache[set][j].tag == tag)
                                                    {
                                                        pos = j;    //对高速缓存行j进行更新
                                                        update(cache[set], HIT, pos, tag);
                                                        find = true;
                                                        break;
                                                    }
                                                }
                                                //如果没有命中,则先检验这个组是否满了,通过维护一个数组occupancy,可以获得组set中有效缓存行的数量
                                                if(!find)
                                                {
                                                    //如果还有空的缓存行,则直接将内存块存放到这个空白行中
                                                    if(occupancy[set] != E)
                                                    {
                                                        occupancy[set]++;    //要加载内存块到一个空缓存行中,占用量+1
                                                        for(int j=0; j
                                                            if(cache[set][j].valid == 0)
                                                            {
                                                                pos = j;
                                                                update(cache[set], MISS, pos, tag);
                                                                break;
                                                            }
                                                        }
                                                    }else   //如果整个组都满了,没有空白行,则此时需要用LRU策略替换掉一个缓存行
                                                    {
                                                        pos = LRU_replace(cache[set]);   //获取要被替换的行号
                                                        update(cache[set], MISS, pos, tag);
                                                        update(cache[set], EVICTION, pos, tag); //更新
                                                    }
                                                }
                                            }
                                            //更新
                                            void update(cache_line* cache_set, enum Category category, int pos, int tag)
                                            {
                                                void update(cache_line* cache_set, enum Category category, int pos, int tag)
                                            {
                                                result[category]++;	//统计相应的缓存情况(命中、不命中、替换)
                                                printf("%s ", category_string[category]);
                                                cache_set[pos].tag = tag;	//更新标志位
                                                cache_set[pos].valid = 1;	//更新有效位
                                                cache_set[pos].timestamp = T;	//更新时间戳
                                                T++;    //每次更新,都要增加时间T
                                            }
                                            }
                                            //LRU替换,暴力搜索时间戳最小的缓存行,返回行号pos
                                            int LRU_replace(cache_line* cache_set)
                                            {
                                                int min = cache_set[0].timestamp;
                                                int pos = 0;
                                                for(int j=1; j
                                                    if(cache_set[j].timestamp 
VPS购买请点击我

免责声明:我们致力于保护作者版权,注重分享,被刊用文章因无法核实真实出处,未能及时与作者取得联系,或有版权异议的,请联系管理员,我们会立即处理! 部分文章是来自自研大数据AI进行生成,内容摘自(百度百科,百度知道,头条百科,中国民法典,刑法,牛津词典,新华词典,汉语词典,国家院校,科普平台)等数据,内容仅供学习参考,不准确地方联系删除处理! 图片声明:本站部分配图来自人工智能系统AI生成,觅知网授权图片,PxHere摄影无版权图库和百度,360,搜狗等多加搜索引擎自动关键词搜索配图,如有侵权的图片,请第一时间联系我们,邮箱:ciyunidc@ciyunshuju.com。本站只作为美观性配图使用,无任何非法侵犯第三方意图,一切解释权归图片著作权方,本站不承担任何责任。如有恶意碰瓷者,必当奉陪到底严惩不贷!

目录[+]