lab4 CSAPP:Cachelab

写在前面

最简单的一集

实验室分为两个部分。在A部分中,实现一个缓存模拟器。在B部分中,编写一个矩阵针对高速缓存性能优化的转置功能。

感觉是比较经典的问题,之前在体系结构的课程中接触过,终于能通过lab实操一下了。

实验目录的 traces 子目录包含参考跟踪文件的集合,将用于评估A部分中编写的缓存模拟器的正确性。跟踪文件是由 Linux 称为valgrind 的程序产生的。

先安装valgrind

复制代码
apt install valgrind

比如 我们用valgrind 捕获 ls -l 的内存访问

复制代码
valgrind --log-fd=1 --tool=lackey -v --trace-mem=yes ls -l

操作-地址-大小 三元组格式

  • I 表示指令读取
  • L 表示数据读取
  • S 表示数据存储
  • M 表示数据修改

PartA:Writing a Cache Simulator

在A部分中,需要在 csim.c 中编写一个缓存模拟器该模拟器将 valgrind 内存跟踪作为输入,模拟此跟踪上的高速缓存存储器的命中/未命中行为,并输出命中,不命中和驱逐的总数目。

需要考虑的问题

1、处理输入参数。

2、模拟缓存行为。

3、考虑LRU(最近最少使用)算法。

下面的程序参考了知乎 林夕丶 的做法

这个实验的pdf 提示通过 getopt() 来解析命令行参数,具体用法问大模型或者查阅文档即可。

需要用到的全局变量、数据结构以及函数:

  • s, E, b:缓存参数,分别表示组索引位数(S = 2^s)、每组行数(E)和块偏移位数(B = 2^b)。
  • T:全局时间戳,用于LRU(最近最少使用)替换策略。(LRU其实可以O(1)维护,但是C语言没有哈希表,所以暴力了)
  • cache:二维数组,表示缓存结构,每个组包含多个行(lineNode)。
  • result[3]:统计命中、未命中和替换次数。
  • lineNode:表示缓存行,包含标签(tag)和时间戳(t)。时间戳用于LRU策略。
  • init():初始化缓存结构。
  • findCache():处理内存访问,判断是否命中、未命中或替换。
  • opt():解析命令行参数,配置缓存参数并打开轨迹文件。
  • setResult():更新统计结果和缓存行状态。
c++ 复制代码
/*===========头文件===============*/
#include "cachelab.h"
#include <fcntl.h>
#include <getopt.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/*===========头文件===============*/

void usage(void) { exit(1); }	// 打印帮助信息可以不实现
int verbose = 0, s, E, b, S, B;  // cache的参数,由命令行输入,opt函数解析

int T = 0;  //时间戳,第一个访存指令的时刻为1,之后每次数据访存都累加1
typedef __uint64_t u64;
typedef unsigned int uint;

// 行的定义
typedef struct lineNode {  
    int t;                 // 时刻
    u64 tag;          // 标记位
} * groupNode;             // 组

enum Category { HIT, MISS, EVICTION };	// 命中 / 缺失 / 驱逐
uint result[3];  // 统计命中、未命中和替换次数
const char *categoryString[3] = {"hit ", "miss ", "eviction "};
groupNode *cache;  // cache 就是若干组
void init();       // 初始化cache
FILE *opt(int argc, char **argv);                           // 解析命令行选项
void findCache(u64 tag, int group_pos, char *result);  // 查询

主函数的逻辑:

  • 解析命令行参数
  • 初始化cache
  • 接收操作并处理
  • pdf要求我们最后打印 命中次数 缺失次数 驱逐次数
c++ 复制代码
int main(int argc, char **argv)
{
    FILE *tracefile = opt(argc, argv);  // 从命令行获取 S E B

    init(); // 根据输入参数初始化cache

    // 接下来处理每一条指令
    char oper[2];
    u64 address;
    int size;  //访问的地址和字节数
    
    while (fscanf(tracefile, "%s %lx,%d\n", oper, &address, &size) == 3) {
        if (oper[0] == 'I') continue;                  // 忽略I
        int group_pos = (address >> b) & ~(~0u << s);  // 从 第 b位开始取,取s位
        u64 address_tag = address >> (b + s);     // b + s之后的位都是
        char resultV[20];                              // 为了 -v 设置的string显示
        memset(resultV, 0, sizeof resultV);
        ++T;
        findCache(address_tag, group_pos, resultV);
        if (oper[0] == 'M') findCache(address_tag, group_pos, resultV);  // M需要两次

        if (verbose) fprintf(stdout, "%s %lx,%d %s\n", oper, address, size, resultV);
    }
    printSummary(result[0], result[1], result[2]);

    return 0;
}

核心函数逻辑:

opt

就是利用 getopt 库函数,解析命令行参数

c 复制代码
FILE *opt(int argc, char **argv)
{
    FILE *tracefile;
    /* Parse the command line 这里c用int是为了保证兼容性,因为有的系统char是unsigned的*/
    for (int c; (c = getopt(argc, argv, "hvsEbt")) != EOF;) {
        switch (c) {
            case 'h': /* print help message */
                usage();
                break;
            case 'v': /* emit additional diagnostic info */
                verbose = 1;
                break;
            case 't': /* 文件 */
                tracefile = fopen(argv[optind], "r");
                if (tracefile == NULL) usage();
                break;
            case 's':  // 组数的位
                s = atoi(argv[optind]);
                if (s <= 0) usage();
                S = 1 << s;
                break;
            case 'E':  // 每一组的行数
                E = atoi(argv[optind]);
                if (E <= 0) usage();
                break;
            case 'b':
                b = atoi(argv[optind]);
                if (b <= 0) usage();
                B = 1 << b;
                break;
        }
    }
    return tracefile;
}

findCache

模拟 cache访问,维护LRU逻辑

c 复制代码
void findCache(u64 tag, int group_pos, char *resultV)
{
    groupNode group = cache[group_pos];

    int min_t_pos = 0, empty_line = -1;
    for (int i = 0; i < E; ++i) {
        struct lineNode line = group[i];
        if (!line.t)
            empty_line = i;  // 有空行
        else {
            if (line.tag == tag) {  // 命中,设置hit
                setResult(group, HIT, tag, i, resultV);
                return;
            }
            if (group[min_t_pos].t > line.t)
                min_t_pos = i;  // 取最小的时刻值,也就是最近最少访问的了
        }
    }
    setResult(group, MISS, tag, empty_line, resultV);
    if (empty_line == -1)  //要读或者写但是没有一个 空行 说明得发生eviction
        setResult(group, EVICTION, tag, min_t_pos, resultV);
}

剩下的都很简单

完整代码:

c 复制代码
/*===========头文件===============*/
#include "cachelab.h"
#include <fcntl.h>
#include <getopt.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/*===========头文件===============*/

void usage(void) { exit(1); }   // 打印帮助信息可以不实现
int verbose = 0, s, E, b, S, B;  // cache的参数,由命令行输入,opt函数解析

int T = 0;  //时间戳,第一个访存指令的时刻为1,之后每次数据访存都累加1
typedef __uint64_t u64;
typedef unsigned int uint;

// 行的定义
typedef struct lineNode {  
    int t;                 // 时刻
    u64 tag;          // 标记位
} * groupNode;             // 组

enum Category { HIT, MISS, EVICTION };  // 命中 / 缺失 / 驱逐
uint result[3];  // 统计命中、未命中和替换次数
const char *categoryString[3] = {"hit ", "miss ", "eviction "};
groupNode *cache;  // cache 就是若干组
void init();       // 初始化cache
FILE *opt(int argc, char **argv);                           // 解析命令行选项
void findCache(u64 tag, int group_pos, char *result);  // 查询

int main(int argc, char **argv)
{
    FILE *tracefile = opt(argc, argv);  // 从命令行获取 S E B

    init(); // 根据输入参数初始化cache

    // 接下来处理每一条指令
    char oper[2];
    u64 address;
    int size;  //访问的地址和字节数
    
    while (fscanf(tracefile, "%s %lx,%d\n", oper, &address, &size) == 3) {
        if (oper[0] == 'I') continue;                  // 忽略I
        int group_pos = (address >> b) & ~(~0u << s);  // 从 第 b位开始取,取s位
        u64 address_tag = address >> (b + s);     // b + s之后的位都是
        char resultV[20];                              // 为了 -v 设置的string显示
        memset(resultV, 0, sizeof resultV);
        ++T;
        findCache(address_tag, group_pos, resultV);
        if (oper[0] == 'M') findCache(address_tag, group_pos, resultV);  // M需要两次

        if (verbose) fprintf(stdout, "%s %lx,%d %s\n", oper, address, size, resultV);
    }
    printSummary(result[0], result[1], result[2]);

    return 0;
}

// 初始化整个cache
void init()
{
    cache = (groupNode *)malloc(sizeof(groupNode) * S);
    for (int i = 0; i < S; ++i) {
        cache[i] = (struct lineNode *)malloc(sizeof(struct lineNode) * E);
        for (int j = 0; j < E; ++j) cache[i][j].t = 0;
    }
}

// category是缓存的种类,resultV是main传下来的,为了verbose的输出
void setResult(groupNode group, enum Category category, int tag, int pos, char *resultV)
{
    ++result[category];
    group[pos].tag = tag;
    group[pos].t = T;
    if (verbose) strcat(resultV, categoryString[category]);
}

// 遍历这个组的所有行,然后看一下是否命中,最后再进行相应的操作即可
void findCache(u64 tag, int group_pos, char *resultV)
{
    groupNode group = cache[group_pos];

    int min_t_pos = 0, empty_line = -1;
    for (int i = 0; i < E; ++i) {
        struct lineNode line = group[i];
        if (!line.t)
            empty_line = i;  // 有空行
        else {
            if (line.tag == tag) {  // 命中,设置hit
                setResult(group, HIT, tag, i, resultV);
                return;
            }
            if (group[min_t_pos].t > line.t)
                min_t_pos = i;  // 取最小的时刻值,也就是最近最少访问的了
        }
    }
    setResult(group, MISS, tag, empty_line, resultV);
    if (empty_line == -1)  //要读或者写但是没有一个 空行 说明得发生eviction
        setResult(group, EVICTION, tag, min_t_pos, resultV);
}

FILE *opt(int argc, char **argv)
{
    FILE *tracefile;
    /* Parse the command line 这里c用int是为了保证兼容性,因为有的系统char是unsigned的*/
    for (int c; (c = getopt(argc, argv, "hvsEbt")) != EOF;) {
        switch (c) {
            case 'h': /* print help message */
                usage();
                break;
            case 'v': /* emit additional diagnostic info */
                verbose = 1;
                break;
            case 't': /* 文件 */
                tracefile = fopen(argv[optind], "r");
                if (tracefile == NULL) usage();
                break;
            case 's':  // 组数的位
                s = atoi(argv[optind]);
                if (s <= 0) usage();
                S = 1 << s;
                break;
            case 'E':  // 每一组的行数
                E = atoi(argv[optind]);
                if (E <= 0) usage();
                break;
            case 'b':
                b = atoi(argv[optind]);
                if (b <= 0) usage();
                B = 1 << b;
                break;
        }
    }
    // printf("-%d 4 -%d 1 -%d 4 -t \n", s, E, b);
    return tracefile;
}

执行结果:

Part B: Optimizing Matrix Transpose

这个part指向性非常明确,我们优化矩阵转置的操作,令其缓存友好。

一共要实现三个转置函数:32×32,64×64,61×67

32×32

考虑分块转置,因为 一个 cache 块的大小为 32字节,即 8个int,我们 8 * 8 分块进行矩阵转置。

组号分布非常的舒服:

注意先把 A 的 每块一行取出来放到临时变量,如果直接赋值,那么会 load 然后 store 两次访存,造成块驱逐。

c++ 复制代码
int i, j;
for (i = 0; i < 32; i += 8) {      
    for (j = 0; j < 32; j += 8) { 
        for (int cnt = 0; cnt < 8; ++cnt, ++i) {
            int temp1 = A[i][j];                 
            int temp2 = A[i][j + 1];
            int temp3 = A[i][j + 2];
            int temp4 = A[i][j + 3];
            int temp5 = A[i][j + 4];
            int temp6 = A[i][j + 5];
            int temp7 = A[i][j + 6];
            int 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;
        }
        i -= 8;
    }
}

64×64

直接用 8 * 8 分块转置,会1:4699

注意到 64 * 64 的组号分布

竖着循环周期是4,所以我们 8 * 8分块,往下填的时候,每4个就把上面四个驱逐,导致命中率极低。

那么我们不妨 改成4 * 4 的分块转置,然后就能通过了。

c 复制代码
void transpose_64(int M, int N, int A[N][M], int B[M][N])
{
    int i, j;
    for (i = 0; i < 64; i += 4) {      
        for (j = 0; j < 64; j += 4) { 
            for (int cnt = 0; cnt < 4; ++cnt, ++i) {  
                int temp1 = A[i][j];                  
                int temp2 = A[i][j + 1];
                int temp3 = A[i][j + 2];
                int temp4 = A[i][j + 3];

                B[j][i] = temp1;  
                B[j + 1][i] = temp2;
                B[j + 2][i] = temp3;
                B[j + 3][i] = temp4;
            }
            i -= 4;
        }
    }
}

61 × 67

组号分布:

因为 61 和 67 俩数都是质数,导致组号分布很难被卡,我们想要直接计算比较好的分块大小很难,所以直接暴力枚举,测试出来17最优

c 复制代码
void transpose_61(int M, int N, int A[N][M], int B[M][N])
{
    int i, j;
    const int BLOCK = 17;
    for (i = 0; i < N; i += BLOCK) {     
        for (j = 0; j < M; j += BLOCK) { 
            for (int x = i; x < N && x < i + BLOCK; ++x)
                for (int y = j; y < M && y < j + BLOCK; ++y) B[y][x] = A[x][y];
        }
    }
}

完整代码:

c 复制代码
/*
 * trans.c - Matrix transpose B = A^T
 *
 * Each transpose function must have a prototype of the form:
 * void trans(int M, int N, int A[N][M], int B[M][N]);
 *
 * A transpose function is evaluated by counting the number of misses
 * on a 1KB direct mapped cache with a block size of 32 bytes.
 */
#include <stdio.h>

#include "cachelab.h"

int is_transpose(int M, int N, int A[N][M], int B[M][N]);

/*
 * transpose_submit - This is the solution transpose function that you
 *     will be graded on for Part B of the assignment. Do not change
 *     the description string "Transpose submission", as the driver
 *     searches for that string to identify the transpose function to
 *     be graded.
 */

// cache参数: 32个组,每组一块,block=32字节

char transpose_submit_desc[] = "Transpose submission";
void transpose_32(int M, int N, int A[N][M], int B[M][N]);
void transpose_64(int M, int N, int A[N][M], int B[M][N]);
void transpose_61(int M, int N, int A[N][M], int B[M][N]);

void transpose_submit(int M, int N, int A[N][M], int B[M][N])
{
    switch (M) {
    case 32:
        transpose_32(M, N, A, B);
        break;
    case 64:
        transpose_64(M, N, A, B);
        break;
    case 61:
        transpose_61(M, N, A, B);
        break;
    }
}

void transpose_32(int M, int N, int A[N][M], int B[M][N])
{
    int i, j;
    for (i = 0; i < 32; i += 8) {      
        for (j = 0; j < 32; j += 8) {  
            for (int cnt = 0; cnt < 8; ++cnt, ++i) {  
                int temp1 = A[i][j];                
                int temp2 = A[i][j + 1];
                int temp3 = A[i][j + 2];
                int temp4 = A[i][j + 3];
                int temp5 = A[i][j + 4];
                int temp6 = A[i][j + 5];
                int temp7 = A[i][j + 6];
                int 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;
            }
            i -= 8;
        }
    }
}

void transpose_64(int M, int N, int A[N][M], int B[M][N])
{
    int i, j;
    for (i = 0; i < 64; i += 4) {      
        for (j = 0; j < 64; j += 4) { 
            for (int cnt = 0; cnt < 4; ++cnt, ++i) {  
                int temp1 = A[i][j];                  
                int temp2 = A[i][j + 1];
                int temp3 = A[i][j + 2];
                int temp4 = A[i][j + 3];

                B[j][i] = temp1;  
                B[j + 1][i] = temp2;
                B[j + 2][i] = temp3;
                B[j + 3][i] = temp4;
            }
            i -= 4;
        }
    }
}
void transpose_61(int M, int N, int A[N][M], int B[M][N])
{
    int i, j;
    const int BLOCK = 17;
    for (i = 0; i < N; i += BLOCK) {     
        for (j = 0; j < M; j += BLOCK) { 
            for (int x = i; x < N && x < i + BLOCK; ++x)
                for (int y = j; y < M && y < j + BLOCK; ++y) B[y][x] = A[x][y];
        }
    }
}

/*
 * You can define additional transpose functions below. We've defined
 * a simple one below to help you get started.
 */

/*
 * trans - A simple baseline transpose function, not optimized for the cache.
 */
char trans_desc[] = "Simple row-wise scan transpose";
void trans(int M, int N, int A[N][M], int B[M][N])
{
    int i, j, tmp;

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

/*
 * registerFunctions - This function registers your transpose
 *     functions with the driver.  At runtime, the driver will
 *     evaluate each of the registered functions and summarize their
 *     performance. This is a handy way to experiment with different
 *     transpose strategies.
 */
void registerFunctions()
{
    /* Register your solution function */
    registerTransFunction(transpose_submit, transpose_submit_desc);

    /* Register any additional transpose functions */
    // registerTransFunction(transpose_submit3, trans_desc);
}

/*
 * is_transpose - This helper function checks if B is the transpose of
 *     A. You can check the correctness of your transpose by calling
 *     it before returning from the transpose function.
 */
int is_transpose(int M, int N, int A[N][M], int B[M][N])
{
    int i, j;

    for (i = 0; i < N; i++) {
        for (j = 0; j < M; ++j) {
            if (A[i][j] != B[j][i]) {
                return 0;
            }
        }
    }
    return 1;
}
相关推荐
jackson凌12 分钟前
【Java学习笔记】运算符
java·笔记·学习
追逐时光者21 分钟前
6种流行的 API 架构风格,你知道几种?
后端
咸鱼求放生24 分钟前
网络请求只到前端页面接口报200并到不到后端接口
java
只会AI搜索得coder32 分钟前
sqlite3 sqlcipher加密,解密,集成springboot,读取sqlcipher加密工具
java·spring boot·sqlite
小麦果汁吨吨吨40 分钟前
Flask快速入门
后端·python·flask
kinlon.liu43 分钟前
SpringBoot整合Redis限流
spring boot·redis·后端
cg50171 小时前
Spring Boot 中的自动配置原理
java·前端·数据库
纪元A梦1 小时前
华为OD机试真题——跳格子3(2025A卷:200分)Java/python/JavaScript/C++/C语言/GO六种最佳实现
java·javascript·c++·python·华为od·go·华为od机试题
IT乐手2 小时前
Java 实现回调监听工具类
java
IT瘾君2 小时前
Java基础:Stream流操作
java