深入理解计算机系统 (CS:APP) - 高速缓存实验 Cache Lab 解析

原文地址:https://billc.io/2019/05/csapp-cachelab/

这个实验是这学期的第四个实验。作为缓存这一章的配套实验,设计得非常精妙。难度上来讲,相比之前的修改现成文件,直接写一个程序也更高了一些。需要注意的是检查程序在编译时开启了 -Werror,需要保证没有警告才能成功编译。

从官方文档得知需要完善 csim.c 和 trans.c 文件,第一个是模拟一个高速缓存的程序并从由 valgrind 程序生成的 trace 文件中统计 hit, miss 和 eviction 的数量。第二个文件需要优化矩阵转置程序降低程序的不命中度。

PART A

这一部分的核心是使用了一个结构体来模拟一个缓存行:

typedef struct {
    int valid;
    ulong tag;
    clock_t time;
} CacheLine;

再通过把缓存行在内存中动态分配成一个二维数组,实现模拟缓存的功能。并且使用了typedef CacheLine *CacheSet; 和 typedef CacheSet *CacheHead; 来让程序更整齐。输入来源于文件和命令行参数。可以用 getopt() 函数来解析参数。

各个函数的作用如下:

  • CacheHead CacheInit(int S, int E) 为缓存动态分配内存;
  • int CacheJudge(CacheHead cache, ulong index, ulong tag) 判断缓存状态,是否有效,标记匹配;
  • void CacheEvict(CacheHead cache, ulong index, ulong tag) 执行 eviction 操作;
  • void CacheTouch(CacheHead cache, ulong index, ulong tag) 执行读取操作,只更新时间戳;
  • void CacheInsert(CacheHead cache, ulong index, ulong tag) 执行缓存写入操作;
  • void Adder(int type, int num) 计数器,增加 hit, miss 和 eviction 的数量,并根据配置选择打印信息;
  • void printByte(bytept h, int len) 逐字节以 16 进制打印内存数据;
  • void Execute(CacheHead cache, char type, ulong address, int len) 主要的执行函数;
  • int main(int argc, char *args[]) main 函数,读取参数,打开文件;

完整的程序代码如下:

// Written By @BillChen
// 2019.5.20
#include "cachelab.h"
#include <getopt.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <unistd.h>

#define MACHINE_BITS 64
#define NEED_EVICT -1
#define NO_MATCH -2
#define CACHED 1
#define ADD_HIT 1
#define ADD_MISS 2
#define ADD_EVICT 3

int totalMissCount = 0;
int totalHitCount = 0;
int totalEvictCount = 0;

typedef unsigned long ulong;
typedef unsigned char *bytept;
const char *optString = "s:E:b:t:hVv";

struct globalOptions {
    int setIndexBits;
    int associativity;
    int blockBits;
    int verboseFlag;
    int tagBits;
    int superVerboseFlag;
    char *traceDir;
} globalOptions;
struct result {
    int hit;
    int miss;
    int evict;
};
typedef struct {
    int valid;
    ulong tag;
    clock_t time;
} CacheLine;

typedef CacheLine *CacheSet;
typedef CacheSet *CacheHead;

void usage() {
    printf("Usage: ./csim [-hv] -s <s> -E <E> -b <b> -t <tracefile>\n");
    printf("-h get help info\n");
    printf("-v Optional verbose flag that displays trace info\n");
    printf("-V Optional super verbose flag that displays very detailed trace info\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\n");
}

CacheHead CacheInit(int S, int E) {
    CacheHead cache;
    cache = calloc(1 << S, sizeof(CacheSet));
    if (cache == NULL) {
        printf("Fail to allocate memory for cache.\n");
        exit(EXIT_FAILURE);
    }
    int i = 0;
    for (i = 0; i < 1 << S; i++) {
        if ((cache[i] = calloc(E, sizeof(CacheLine))) == NULL) {
            printf("Fail to allocate memory for cache.\n");
            exit(EXIT_FAILURE);
        }
    }
    for (i = 0; i < 1 << S; i++) {
        int j;
        for (j = 0; j < E; j++) {
            cache[i][j].valid = 0;
        }
    }
    return cache;
}

int CacheJudge(CacheHead cache, ulong index, ulong tag) {
    int i;
    int fullFlag = 1;
    int matchFlag = 0;
    for (i = 0; i < globalOptions.associativity; i++) {
        if (cache[index][i].valid == 0) {
            fullFlag = 0;
        }
        if (cache[index][i].tag == tag && cache[index][i].valid == 1) {
            matchFlag = 1;
        }
    }
    if (matchFlag == 1)
        return CACHED;
    if (fullFlag == 1)
        return NEED_EVICT;
    else
        return NO_MATCH;
}

void CacheInsert(CacheHead cache, ulong index, ulong tag) {
    int freeLine = 0, i;
    for (i = 0; i < globalOptions.associativity; i++) {
        if (cache[index][i].valid == 0)
            break;
        freeLine++;
    }
    CacheLine *target = cache[index] + freeLine;
    target->tag = tag;
    target->valid = 1;
    target->time = clock();
}

void CacheEvict(CacheHead cache, ulong index, ulong tag) {
    int firstLine = 0, i = 0;
    clock_t firstCachedTime = cache[index][i].time;
    for (i = 0; i < globalOptions.associativity; i++) {
        if (cache[index][i].time < firstCachedTime) {
            firstCachedTime = cache[index][i].time;
            firstLine = i;
        }
    }
    CacheLine *target = cache[index] + firstLine;
    target->tag = 0;
    target->time = 0;
    target->valid = 0;
}

void CacheTouch(CacheHead cache, ulong index, ulong tag) {
    int touchLine = 0;
    while (cache[index][touchLine].tag != tag)
        touchLine++;
    cache[index][touchLine].time = clock();
}

void Adder(int type, int num) {
    int v = globalOptions.verboseFlag;
    switch (type) {
    case ADD_EVICT:
        totalEvictCount += num;
        if (v && num != 0)
            printf("eviction ");
        break;
    case ADD_HIT:
        totalHitCount += num;
        if (v && num != 0)
            printf("hit ");
        break;
    case ADD_MISS:
        totalMissCount += num;
        if (v && num != 0)
            printf("miss ");
    }
}

void printByte(bytept h, int len) {
    int i;
    for (i = 0; i < len; i++)
        printf("%.2x ", h[i]);
    printf("\n");
}

void Execute(CacheHead cache, char type, ulong address, int len) {
    ulong index = (address << globalOptions.tagBits) >> (MACHINE_BITS - globalOptions.setIndexBits);
    ulong tag = address >> (globalOptions.blockBits + globalOptions.setIndexBits);
    int status = CacheJudge(cache, index, tag);
    if (globalOptions.verboseFlag == 1) {
        if(globalOptions.superVerboseFlag == 1){
            printf("\n[address:] ");
            printByte((bytept)&address, sizeof(long));
            printf("[index:] ");
            printByte((bytept)&index, sizeof(long));
            printf("[tag:] ");
            printByte((bytept)&tag, sizeof(long));
            printf("(Decimal)[index: %ld, tag: %ld]\n------------------------------------------- ", index, tag);
        }
        else{
            printf("(Decimal)[index: %ld, tag: %ld] ------ ", index, tag);
        }
    }
    switch (status) {
    case CACHED:
        CacheTouch(cache, index, tag);
        if (type == ‘M‘) {
            Adder(ADD_HIT, 1);
            Adder(ADD_HIT, 1);
        } else {
            Adder(ADD_HIT, 1);
        }
        break;
    case NO_MATCH:
        CacheInsert(cache, index, tag);
        if (type == ‘M‘) {
            Adder(ADD_MISS, 1);
            Adder(ADD_HIT, 1);
        } else {
            Adder(ADD_MISS, 1);
        }
        break;
    case NEED_EVICT:
        CacheEvict(cache, index, tag);
        CacheInsert(cache, index, tag);
        if (type == ‘M‘) {
            Adder(ADD_MISS, 1);
            Adder(ADD_EVICT, 1);
            Adder(ADD_HIT, 1);

        } else {
            Adder(ADD_MISS, 1);
            Adder(ADD_EVICT, 1);
        }
        break;
    default:
        printf("Unknown error.\n");
        exit(EXIT_FAILURE);
    }
    if (globalOptions.verboseFlag == 1) {
        printf("\n");
    }
}

int main(int argc, char *args[]) {
    char ch;
    while ((ch = getopt(argc, args, optString)) != -1) {
        switch (ch) {
        case ‘s‘:
            if (atoi(optarg) < 0) {
                printf("Unvalid input for <s>. Try Again.\n");
                exit(EXIT_FAILURE);
            }
            globalOptions.setIndexBits = atoi(optarg);
            break;
        case ‘E‘:
            if (atoi(optarg) < 0) {
                printf("Unvalid input for <E>. Try Again.\n");
                exit(EXIT_FAILURE);
            }
            globalOptions.associativity = atoi(optarg);
            break;
        case ‘b‘:
            if (atoi(optarg) < 0) {
                printf("Unvalid input for <b>. Try Again.\n");
                exit(EXIT_FAILURE);
            }
            globalOptions.blockBits = atoi(optarg);
            break;
        case ‘t‘:
            globalOptions.traceDir = optarg;
            break;
        case ‘v‘:
            globalOptions.verboseFlag = 1;
            break;
        case ‘h‘:
            usage();
            exit(EXIT_FAILURE);
        case ‘V‘:
            globalOptions.verboseFlag = 1;
            globalOptions.superVerboseFlag = 1;
            break;
        default:
            usage();
            exit(EXIT_FAILURE);
            break;
        }
    }
    globalOptions.tagBits = MACHINE_BITS - globalOptions.blockBits - globalOptions.setIndexBits;

    FILE *traceFile = fopen(globalOptions.traceDir, "r");
    if (traceFile == NULL) {
        printf("Fail to open file: %s\n", globalOptions.traceDir);
        exit(EXIT_FAILURE);
    }
    CacheHead cache = CacheInit(globalOptions.setIndexBits, globalOptions.associativity);
    char traceLine[32];
    while (fgets(traceLine, 32, traceFile) != NULL) {
        char mode;
        ulong address;
        int len;
        sscanf(traceLine, " %c %lx,%d", &mode, &address, &len);
        if (mode == ‘I‘)
            continue;
        if (globalOptions.verboseFlag == 1) {
            printf("%c %lx,%d ", mode, address, len);
        }
        Execute(cache, mode, address, len);
    }
    printSummary(totalHitCount, totalMissCount, totalEvictCount);
    free(cache);
    return 0;
}

最终在 ./driver.py 的测试下,该程序和 csim-ref 的运行结果一致。

PART B

按照官方文档的说明,需要在 trans.c 中写入一个优化的矩阵转置函数。尽可能地降低不命中率。使用命令 ./test-trans -M <rol> -N <col> 可以查看这一转置函数的不命中数。生成的 trace.fi 文件还可以利用 PART A 写的缓存模拟器检查命中情况。

从官方文档得知要在 PART B 中得到分数需要完成三个测试并满足对应的不命中数条件。

Test I: 32 * 32

由于程序使用的缓存 block size 为 5,也就是有 2^5 的块大小,为32字节。sizeof(int) = 4,所以可以存储下 8 个整数。

先研究原始的一个简单的矩阵转置函数:

int i, j, tmp;
for (i = 0; i < N; i++) {
    for (j = 0; j < M; j++) {
        tmp = A[i][j];
        B[j][i] = tmp;
    }
}

这一函数的运行结果出现了 1000 多个 miss。提取一小部分原始的文件,利用 csim 查看详细的 miss 和 eviction 信息,可以发现在读取的时候发生了严重的抖动,导致了大量 miss 的出现。

所以可以利用矩阵分块的思想。每一行数组都可以被存入 4 个缓存行中,一共有 32 个缓存行,所以每过 8 行就会出现一次和前面相同的组索引,发生 miss 和 eviction。所以考虑将 32 * 32 的矩阵分成 16 个 8 * 8 的矩阵,每一次都将一行的 8 个 int 分别存储进 t1 – t4。

即,将矩阵划分成如下结构:

1 2 3 4
5 6 7 8
9 10 11 12
13 14 15 16

其中每一个小块都是 8 * 8,每一行能够完整存储到缓存行中的矩阵。这种情况在 transpose_submit() 中的代码如下:

if(N == 32 && M == 32){
    int i, j, k;
    int t1, t2, t3, t4, t5, t6, t7, t8;
    for (i = 0; i < 32; i += 8) {
        for (j = 0; j < 32; j += 8) {
            for (k = 0; k < 8; k++) {
                t1 = A[i + k][j];
                t2 = A[i + k][j + 1];
                t3 = A[i + k][j + 2];
                t4 = A[i + k][j + 3];
                t5 = A[i + k][j + 4];
                t6 = A[i + k][j + 5];
                t7 = A[i + k][j + 6];
                t8 = A[i + k][j + 7];
                B[j][i + k] = t1;
                B[j + 1][i + k] = t2;
                B[j + 2][i + k] = t3;
                B[j + 3][i + k] = t4;
                B[j + 4][i + k] = t5;
                B[j + 5][i + k] = t6;
                B[j + 6][i + k] = t7;
                B[j + 7][i + k] = t8;
            }
        }
    }
}

结果如下图所示:

Test II: 64 * 64

和第一种情况测试类似。但是由于大小变成了 64 * 64,每过 4 行就会出现一次冲突的情况。所以可以先分成 8 * 8 的块,然后再把 8 * 8 的块分成 4 个 4 * 4 的块。读取一行,但存储进的位置如图所示。逆序存储之后再逐行处理 C’ 和 B’ 处的数据。

由于之前是逆序存储的,所以在 C’ 会把 0 加载进缓存,而在 B’ 会把 24 加载进缓存,再利用 t1, t2, t3, t4 四个变量作临时变量存储,交换 0 行和 24 行的位置。

这一部分比较复杂,这里参考了欧阳松的博客(https://www.ouyangsong.com/posts/55291/#fn4),大概的逻辑如下图所示:

具体的代码实现如下:

else if (N == 64 && M == 64) {
    int t0, t1, t2, t3, t4, t5, t6, t7;
    for (int i = 0; i < N; i += 8) {
        for (int j = 0; j < M; j += 8) {
            for (int k = i; k < i + 4; k++) {
                t0 = A[k][j];
                t1 = A[k][j + 1];
                t2 = A[k][j + 2];
                t3 = A[k][j + 3];
                t4 = A[k][j + 4];
                t5 = A[k][j + 5];
                t6 = A[k][j + 6];
                t7 = A[k][j + 7];
                B[j][k] = t0;
                B[j + 1][k] = t1;
                B[j + 2][k] = t2;
                B[j + 3][k] = t3;
                B[j + 0][k + 4] = t7;
                B[j + 1][k + 4] = t6;
                B[j + 2][k + 4] = t5;
                B[j + 3][k + 4] = t4;
            }
            for (int h = 0; h < 4; h++) {
                t0 = A[i + 4][j + 3 - h];
                t1 = A[i + 5][j + 3 - h];
                t2 = A[i + 6][j + 3 - h];
                t3 = A[i + 7][j + 3 - h];
                t4 = A[i + 4][j + 4 + h];
                t5 = A[i + 5][j + 4 + h];
                t6 = A[i + 6][j + 4 + h];
                t7 = A[i + 7][j + 4 + h];
                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];
                B[j + 3 - h][i + 4] = t0;
                B[j + 3 - h][i + 5] = t1;
                B[j + 3 - h][i + 6] = t2;
                B[j + 3 - h][i + 7] = t3;
                B[j + 4 + h][i + 4] = t4;
                B[j + 4 + h][i + 5] = t5;
                B[j + 4 + h][i + 6] = t6;
                B[j + 4 + h][i + 7] = t7;
            }
        }
    }
}

得到如下结果:

Test III: 61 * 67

这一测试中由于矩阵不规则,而且也不是 8 的倍数,所以在行与行之间没有特别明显的冲突不命中的关系。可以尝试用分块矩阵的方式优化。经过尝试 8 * 8 的分块和 16 * 16 的分块后,发现使用 16 * 16 的分块方式可以将 miss 数降低到 2000 以下。

这一部分的代码如下:

else {
    int i, j, k, h;
    for (i = 0; i < N; i += 16) {
        for (j = 0; j < M; j += 16) {
            for (k = i; k < i + 16 && k < N; k++) {
                for (h = j; h < j + 16 && h < M; h++) {
                    B[h][k] = A[k][h];
                }
            }
        }
    }
}

可以得到 1992 的 miss 数。

最终在 ./driver.py 的运行结果中,Part B 获得如下结果:



本实验的完整代码可以在这里下:https://github.com/BillChen2000/LearningRepo/blob/master/Course/CSAPP/LAB4/billchen-handin.tar

一如既往地,现在又是凌晨了 orz.

2019.5.22

原文地址:https://www.cnblogs.com/BillChen2000/p/12534013.html

时间: 2024-10-03 00:20:18

深入理解计算机系统 (CS:APP) - 高速缓存实验 Cache Lab 解析的相关文章

深入理解计算机系统 (CS:APP) Lab2 - Bomb Lab 解析

原文地址:https://billc.io/2019/04/csapp-bomblab/ 写在前面 CS:APP是这学期的一门硬核课程,应该是目前接触到最底层的课程了.学校的教学也是尝试着尽量和CMU同步,课件和习题都直接照搬原版.包括现在着手的第二个实验室Bomb Lab.这个lab很有意思,没有提供全部c语言代码,需要手动根据反汇编语言推测在每一个阶段需要输入的内容,输入正确就可以进入下一个阶段. 理论上每个人获取到的lab都是不一样的,但对于自学学生而言在官网http://csapp.cs

深入理解计算机系统(CSAPP)实验二 datalab-handout

实验的目的是 填写 bits.c里面的函数,使其按照规定的要求(比如只能使用有限且规定的操作符和数据类型,不能使用控制语句等等)实现函数的功能. 同时 dlc文件是用来检测 bits.c 里面的函数是否  是按照要求编写的,有没有使用非法的数据类型等. 使用方法:./dlc bits.c 检测成功后,使用 btest 测试 每一个函数功能方面是否正确无误.使用方法:./btest,如果某个函数错误,会显示错误的数据,以及正确的数据. 完整的bits.c如下.也是参考网上各路大神的. /* * C

深入理解计算机系统BombLab实验报告

又快有一个月没写博客了,最近在看<深入理解计算机系统>这本书,目前看完了第三章,看完这章,对程序的机器级表示算是有了一个入门,也对 C 语言里函数栈帧有了一个初步的理解. 为了加深对书本内容的认识,以后每学习完一部分章节,就完成相应书本附带的实验题目. 第三章对应的实验是 BombLab,下面是我做这个实验的过程. BombLab 分为 6 个普通关卡和一个隐形关卡,为了开始闯关,得先弄清楚从哪里开始行动. 首先使用 objdump 命令 objdump -t bomb > bomb_s

《深入理解计算机系统》 第一章读书笔记

最近开始啃CSAPP,买了英文版,看得比较慢.只有先理解系统才能在更多方面学的更明其实质 Chapter1: * 一份hello.c代码的编译分为四个阶段:1.Pre-processor:编译预处理,就是把#include后面的库代码加入到程序中,生成hello.i的文件. 2.Complier:用编译器把hello.i的C代码翻译成汇编语言,并生成:hello.s文件.(汇编语言是高级语言转为机器码的一个中间过程) 3.Assembler:汇编机把汇编语言翻译成机器二进制代码,生成hello.

2018-2019-1 20189215 《深入理解计算机系统》第一章

第一章 计算机系统漫游 主要内容 全面精炼的概括了本书的内容,也就是"计算机系统概述",包括: 1.解释计算机系统中"信息"的概念:就是二进制位: 2.解释源程序(以C源程序为例)到可执行程序的过程:预处理 → 编译 → 汇编 → 链接 3.通过可执行程序被shell加载执行流程,讲解了计算机的硬件组成: 总线.I/O设备.主存.处理器. 4.高级缓存的作用以及计算机存储设备的体系结构--金字塔层次结构. 5.解释了操作系统.应用进程.计算机硬件三者之间的关系:操作

《深入理解计算机系统》第六周学习笔记

第四章 处理器体系结构 (一)知识点总结 一.Y86指令集体系结构 1.Y86处理器状态类似于IA32,有8个程序寄存器: %eax.%ecx.%edx.%ebx.%esi.%edi.%esp.%ebp.处理器的每个程序寄存器存储一个字.%esp被入栈.出栈.调用和返回指令作为栈指针. 2.3个一位的条件吗:ZF.SF.OF,它们保存最近的算术或逻辑指令所造成影响的有关信息.程序计数器PC存放当前正在执行指令的地址. 3.程序状态的最后一个部分是状态码stat,它表明程序执行的总体状态 4.Y8

深入理解计算机系统读书笔记一 ---&gt; 计算机基础漫游

一.程序编译的不同阶段. 通常我们是以高级程序开发易于阅读的代码,我们通过语法规则推断代码的具体含义.但是计算机执行代码的时候就需要把代码解析成既定的可执行问题,计算机是如何处理的呢?这里以C语言hello.c文件为例来说明中间过程. #include <stdio.h> int main() { printf("hello world!\n"); } 先上张图. C语言源程序----预处理解析头文件和函数  --- 编译器解析成汇编语言 ---   翻译机器语言指令,打包

电子书 深入理解计算机系统.pdf

内容简介 和第2版相比,本版内容上*大的变化是,从以IA32和x86-64为基础转变为完全以x86-64为基础.主要更新如下: 基于x86-64,大量地重写代码,首次介绍对处理浮点数据的程序的机器级支持. 处理器体系结构修改为支持64位字和操作的设计. 引入更多的功能单元和更复杂的控制逻辑,使基于程序数据流表示的程序性能模型预测更加可靠. 扩充关于用GOT和PLT创建与位置无关代码的讨论,描述了更加强大的链接技术(比如库打桩). 增加了对信号处理程序更细致的描述,包括异步信号安全的函数等. 采用

计算机编程基础之深入理解计算机系统1

目录 概述——<深入理解计算机系统> 计算机系统漫游 信息的表示和处理 概述——<深入理解计算机系统> Computer Systems A Programmers Perspective  英文名 计算机系统漫游 本章简介 当系统上执行hello程序时,系统发生了什么以及为什么会这样 信息就是位+上下文 源程序(或者源文件) hello.c,实际上是由值0和1组成的位(bit)序列,8个位被组织成一组,成为字节.每个字节表示程序中某个文本字符,大部分的现代系统都使用ASCII标准