深入理解计算机系统-cachelab详解

开始

这个lab有两个部分,第一个部分要求我们写一个缓存模拟器。第二部分要求我们优化矩阵转置的函数,使它的缓存脱靶数降到最低。

Part A

其实这个模拟器不必从头开始写。反正作者已经给我们写好了 csim-ref 对不对?我们只要把它反汇编了,之后把汇编翻译成C语言代码就行了。好了,我们看 Part B.
嘿嘿,开个玩笑而已。不过如果你有足够耐心的话,这种办法真的可以一试……

1. 要用到的几个结构

要写这个模拟程序,我们先从缓存的结构看起。我们知道,缓存的结构是下面这个样子:
在这里插入图片描述

所以,我们应该先定义一个结构,用来描述缓存的各个属性。比如下面这个结构:

typedef struct per_block
{
   
    bool isVaild;
    int tag; 
    // 从测试用的指令以及缓存尺寸来看,
    // tag 为 32 位是可行的。
    int timestamp;
} per_block;

这个结构描述的是缓存中一个 set 里的一行,外加一个“时间戳”,以记录上一次访问这一条缓存是什么时候。为什么我们不存储这一行的内容呢?这是因为,我们的缓存模拟器只需要计算有多少脱靶数、命中数、驱逐数,并不需要读取具体的字节。
刚才我们定义了存储一个 set 中一行的结构。接下来我们看一看怎么把一个 set 中的若干个 per_block 联系在一起。办法很简单,用指向 per_block 的指针就可以了。不过 per_block* 看起来似乎有些表意不明,于是我们给它起一个别名,叫 set:

typedef per_block* set;

那么若干个 set 合起来是什么呢?没错,就相当于一个完整的缓存。我们当然可以直接把缓存定义为指向 set 的指针,之后再给 set* 起别名;不过呢,表示缓存的结构,除了可以有指向 set 的指针,更可以有 set 数、line 数对不对。所以我们把表示缓存的结构定义成这个样子:

typedef struct cache
{
   
    int nlines; // line 数
    int nsets;  // set 数
    int bblocks; 
    // 在某个具体的地址中,用来表示 block 偏移量的比特数
    int bsets;
    // 在某个具体的地址中,用来表示 set 索引的比特数
    set* head;
} cache;

我们还需要用一个结构,表示测试文件中的每一个指令。如下:

typedef struct operation
{
   
    char op; // 什么操作?'I', 'M', 'S', 还是 'L'?
    int size; // 访问了几个字节?
    // 事实上,因为内存中的数据都是对齐的
    // (也就是说,不会出现某一行能放 8 个字节,
    // 某个指令却从该行第 5 个字节开始连续读 6 个字节的情况),
    // 不包含这个字段不会对后面命中数之类的计算产生任何影响。
    // 这里包含它是为了保证,给出参数 "-v" 时,csim 的输出
    // 和参考用的模拟器输出一致。
    unsigned long address; // 访问的字节从哪个地址开始?
} operation;

2. 基本的操作!

对于我们的缓存模拟器来说,最最基本的操作就是从一个特定的地址中获得 set 的索引和 tag 值(block 的偏移量倒还在其次),也就是下面两个函数所做的:

static inline unsigned gets_s(unsigned long address, int s, int b)
{
   
    return (address >> b) % (1 << s);
}

static inline unsigned gets_t(unsigned long address, int s, int b)
{
   
    return address >> (s + b);
}

我们知道,对于十进制数 n n n 来说,把它除上 1 0 r 10^r 10r 取余数,可以得到它的末 r r r 位数。比如,1234 除 1 0 2 10^2 102 的余数是 34,正好是它的末两位。对于二进制数来说也同理——把一个二进制数除上 2 s 2^s 2s 位取余数,就可以得到它的末 s s s 位。gets_s()就利用了这个性质来取得地址中包含的 set 索引。
而 gets_t() 的原理就简单啦~ 要获得 tag 值,只要把地址中的 set 索引和 block 偏移量移走就行了。
之后我们为模拟缓存分配空间。参数 s、b 和 e 的含义和书中的相同。

cache allocate_set(int s, int b, int e)
{
   
    int nsets = 1 << s;
    set* head = calloc(nsets, sizeof(set));
    for (int i = 0; i < nsets; ++i)
        head[i] = calloc(e, sizeof(per_block));

    cache c = {
    e, nsets, b, s, head };
    return c;
}

与 allocate_set() 配套的是 free_set():

void free_set(cache* c)
{
   
    for (int i = 0; i < c->nsets; ++i)
    {
   
        free(c->head[i]);
        c->head[i] = NULL;
    }
    free(c->head);
    c->head = NULL;
}

参数用指向 cache 的指针是为了降低参数传递过程中的开销。
接下来是从文件中提取操作的函数。我们不会试图一下子就把文件中的所有操作载入内存(否则,我们必须考虑在文件过大的情况下,内存不够用的情况);相反地,我们一次只读入一个操作:

operation fetch_operation(FILE* pfile) 
// pfile 指向一个已经打开的测试文件。下面我们还会提到它。
{
   
    char temp[OP_LENGTH]; // OP_LENGTH 被定义为 128
    operation oper = {
    0, 0, 0 };
    if(!fgets(temp, OP_LENGTH, pfile))
        return oper;
    sscanf(temp, "\n%c %lx,%d", &oper.op, &oper.address, &oper.size);
    // 这里,'\n' 的含义是“忽略此处可能出现的空白”
    return oper;
}

好啦,现在我们已经写好了创建缓存、释放缓存和抓取指令的三个函数。接下来我们要写的是处理抓取来的指令的函数。不过在真正开始之前,我们首先来回顾一下这些操作的一般流程——
首先要先根据地址找到对应的 set;之后呢,要在这个 set 的各行里搜索对应的 tag 值,确定之前是不是已经把这一行载入了;如果找到,那么恭喜,接下来就可以执行相应的操作了;如果没有找到呢,就要在这个 set 里找目前还未加载任何内容的行(也就是 valid bit 为零的行;放在我们的模拟器中,就是 isValid == false)。如果这个 set 全满了,那就用 LRU 算法,找到这个 set 中使用的最不频繁的行,用新的一行把它覆盖掉,再执行相关的操作。
对于 Store 和 Load,它们前期向缓存加载地址的流程都是上面那个样子,不同的只是后期对具体内容的操作——然而我们并不关心它。至于 Modify 操作嘛,只是 Store 和 Load 的简单组合。
因此,只要把上面通用流程中的子过程都写好,就可以完成一大半工作。我们先来看搜索对应 tag 值的函数:

per_block* find_match_block(set s, int e, int tag)
{
   
    for (int i = 0; i < e; ++i)
        if (s[i].tag == tag && s[i].isVaild)
            return s + i;
    return NULL;
}

for 循环遍历 set 中的每一行,以搜索对应的 tag 值。如果找到,就返回指向对应行的指针;如果没有找到,就返回 NULL。
接下来是搜索“新行”的函数:

per_block* find_new_block(set s, int e)
{
   
    for (int i = 0; i < e; ++i)
        if (!(s[i].isVaild))
            return s + i;

    return NULL;
}

这个函数不难理解——就是返回遍历过程中遇到的第一个“新行”。
最后一个要写的函数,要根据 LRU 算法搜索访问时间最靠前的行。要做到这一点,就需要用到前面 per_block 结构里定义的 timestamp——找到值最小的行返回就可以了。

per_block* find_evict_block(set s, int e)
{
   
    int index = 0;
    for (int i = 1; i < e; ++i)
        if (s[i].timestamp < s[index].timestamp)
            index = i;
    return s + index;
}

接下来就要实现 Load、Store 操作啦。为了实现 csim-ref 中 “-v” 开关对应的功能,我们另外加一个 isDisplay 参数,用来指示是否输出相关信息。为什么用指针传递 c 和 op 呢?同样是出于降低参数调用过程中开销的考虑。

void load_or_store(cache* c, operation* op, bool isDisplay)
{
   
    int s = gets_s(op->address, c->bsets, c->bblocks);
    int t = gets_t(op->address, c->bsets, c->bblocks);
    
    if (isDisplay)
        printf("%c %lx,%d ", op->op, op->address, op->size);

    per_block* appropriate_tag = find_match_block(c->head[s], c->nlines, t);
    if (appropriate_tag)
    {
   
        ++hit_count;
        // hit_count 和下面的 eviction_count 、miss_count 一样,
        // 都是全局变量。

        if (isDisplay)
            printf("hit ");
    }
    else
    {
   
        ++miss_count;
        if (isDisplay)
            printf("miss ");

        appropriate_tag = find_new_block(c->head[s], c->nlines);
        if (appropriate_tag)
            appropriate_tag->isVaild = true;
        else
        {
   
            ++eviction_count;
            appropriate_tag = find_evict_block(c->head[s], c->nlines);
            if (isDisplay)
                printf("eviction ");
        }

        appropriate_tag->tag = t;
    }

    appropriate_tag->timestamp = timestamp;
    timestamp++;
    // 最后更新时间戳。其实不必非要用真实的时间~ 能反映出访问的先后顺序就好。

    if (isDisplay)
        printf("\n");
}

接下来是 Modify 操作:

void modify(cache* c, operation* op, bool isDisplay)
{
   
    if (isDisplay)
        printf("%c %lx,%d ", op->op, op->address, op->size);
    op->op = 'L';
    load_or_store(c, op, isDisplay);
    op->op = 'S';
    load_or_store(c, op, isDisplay);
    if (isDisplay)
        printf("\n");
}

很好理解对不对?
当然这里用一个很不好的问题——调用 modify 时,如果 isDisplay 为 true,那么 op 结构中的内容将会被显示 3 次。解决的方法很简单——只要给 load_or_store 再添加一个参数 isCalledByMain 就可以了。就像下面这样:


                
  • 21
    点赞
  • 91
    收藏
    觉得还不错? 一键收藏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值