Solution

Part A

代码

#include <getopt.h>
#include <stdio.h>
#include <stdlib.h>
#include "cachelab.h"

/* defines */

#define OPTSTRING "hvs:E:b:t:"
#define INF 2147483647
#define HELP_MESSAGE                                             \
    "Usage: ./csim [-hv] -s <num> -E <num> -b <num> -t <file>\n" \
    "Options:\n"                                                 \
    "  -h         Print this help message.\n"                    \
    "  -v         Optional verbose flag.\n"                      \
    "  -s <num>   Number of set index bits.\n"                   \
    "  -E <num>   Number of lines per set.\n"                    \
    "  -b <num>   Number of block offset bits.\n"                \
    "  -t <file>  Trace file.\n"                                 \
    "\n"                                                         \
    "Examples:\n"                                                \
    "  linux>  ./csim -s 4 -E 1 -b 4 -t traces/yi.trace\n"       \
    "  linux>  ./csim -v -s 8 -E 2 -b 4 -t traces/yi.trace"

typedef struct Line {
    int valid;
    unsigned long tag;
    int timeStamp;
} Line;

typedef struct Set {
    Line *lines;
} Set;

int time = 0;

int newTimeStamp() {
    time++;
    return time;
}

struct Cache {
    int s;
    int E;
    int b;
    Set *sets;
} C;

int hitCount = 0;
int missCount = 0;
int evictionCount = 0;

/* enums */

enum OutputFlag : int { VERBOSE, SILENT } outputMode = SILENT;

/* M for miss, E for eviction, H for hit */
typedef enum : int {
    M = 1 << 0,
    E = 1 << 1,
    H = 1 << 2,
} CheckFlag;

/* inits */

/* initialize the cache, use malloc */
void initCache(int s, int E, int b) {
    C.s = s;
    C.E = E;
    C.b = b;
    C.sets = (Set *)malloc(sizeof(Set) * ((size_t)1 << s));
    for (unsigned long i = 0; i < (unsigned long)1 << s; i++) {
        C.sets[i].lines = (Line *)malloc(sizeof(Line) * E);
        for (int j = 0; j < E; j++) {
            C.sets[i].lines[j].valid = 0;
            C.sets[i].lines[j].tag = 0;
            C.sets[i].lines[j].timeStamp = time;
        }
    }
}

/* outputs */

/* verbosely output the flag */
void verbosely(CheckFlag flag) {
    if (flag & M) {
        printf("miss ");
    }
    if (flag & E) {
        printf("eviction ");
    }
    if (flag & H) {
        printf("hit ");
    }
}

/* processes */

/*
 * process the flag
 * add to the corresponding counter
 */
void processFlag(CheckFlag flag) {
    hitCount += !!(flag & H);
    missCount += !!(flag & M);
    evictionCount += !!(flag & E);

    if (outputMode == VERBOSE) {
        verbosely(flag);
    }
}

/* use bit tricks to get the set index of the address */
unsigned long getSetIndex(unsigned long address) {
    return (address >> C.b) & (((unsigned long)1 << C.s) - 1);
}

/* use bit tricks to get the tag of the address */
unsigned long getTag(unsigned long address) {
    return address >> (C.s + C.b);
}

/* process a load operation */
void processLoad(unsigned long address) {
    unsigned long setIndex = getSetIndex(address);
    unsigned long tag = getTag(address);

    int lruId = 0, lruTimeStamp = INF;
    for (int i = 0; i < C.E; i++) {
        Line *l = &C.sets[setIndex].lines[i];
        if (l->valid && l->tag == tag) {
            l->timeStamp = newTimeStamp();
            processFlag(H);
            return;
        }

        if (lruTimeStamp > l->timeStamp) {
            lruTimeStamp = l->timeStamp;
            lruId = i;
        }
    }

    CheckFlag flag = M;
    Line *lruL = &C.sets[setIndex].lines[lruId];
    if (lruL->valid) {
        flag |= E;
    }

    lruL->valid = 1;
    lruL->tag = tag;
    lruL->timeStamp = newTimeStamp();

    processFlag(flag);
}

/*
 * process a store operation
 * the same as load operation
 */
void processStore(unsigned long address) {
    processLoad(address);
}

/*
 * process a modify operation
 * first load then modify
 */
void processModify(unsigned long address) {
    processLoad(address);
    processStore(address);
}

/* process a line in the trace file */
void processLine(char *line) {
    if (line[0] != ' ') {
        return;
    }

    char type;
    unsigned long address;
    int size;
    if (sscanf(line, " %c %lx,%d", &type, &address, &size) != 3) {
        printf("invalid line: %s\n", line);
        exit(EXIT_FAILURE);
    }

    if (outputMode == VERBOSE) {
        printf("%c %lx,%d ", type, address, size);
    }
    switch (type) {
        case 'L':
            processLoad(address);
            break;
        case 'S':
            processStore(address);
            break;
        case 'M':
            processModify(address);
            break;
        default:
            printf("invalid type: %c\n", type);
            exit(EXIT_FAILURE);
            break;
    }

    if (outputMode == VERBOSE) {
        puts("");
    }
}

/* implement the function that is called by main */
void implement(int argc, char *argv[]) {
    char opt;
    FILE *fp;
    int s, E, b;
    while ((opt = getopt(argc, argv, OPTSTRING)) != -1) {
        switch (opt) {
            case 'h':
                printf(HELP_MESSAGE);
                exit(EXIT_SUCCESS);
                break;
            case 'v':
                outputMode = VERBOSE;
                break;
            case 's':
                s = atoi(optarg);
                break;
            case 'E':
                E = atoi(optarg);
                break;
            case 'b':
                b = atoi(optarg);
                break;
            case 't':
                fp = fopen(optarg, "r");
                if (fp == NULL) {
                    printf("cannot open");
                    exit(EXIT_FAILURE);
                }
                break;
            default:
                break;
        }
    }

    if (s == 0 || E == 0 || b == 0 || fp == NULL) {
        printf("missing argument\n");
        exit(EXIT_FAILURE);
    }
    initCache(s, E, b);

    while (fp != NULL) {
        char buf[100];
        if (fgets(buf, sizeof(buf), fp) == NULL) {
            break;
        }
        processLine(buf);
    }
}

int main(int argc, char *argv[]) {
    implement(argc, argv);
    printSummary(hitCount, missCount, evictionCount);
    return 0;
}

Defines

OPTSTRING
#define OPTSTRING "hvs:E:b:t:"

描述

OPTSTRING 是用于解析命令行选项的字符串。它包含了可用选项的列表,每个选项由一个字母表示,后面可以跟随一个冒号表示该选项需要一个参数。

INF
#define INF 2147483647

描述

INF 是一个表示正无穷大的常量。在程序中,它用于表示一个很大的时间戳值,用于标识最近的时间戳。

HELP_MESSAGE
#define HELP_MESSAGE                                             \
    "Usage: ./csim [-hv] -s <num> -E <num> -b <num> -t <file>\n" \
    "Options:\n"                                                 \
    "  -h         Print this help message.\n"                    \
    "  -v         Optional verbose flag.\n"                      \
    "  -s <num>   Number of set index bits.\n"                   \
    "  -E <num>   Number of lines per set.\n"                    \
    "  -b <num>   Number of block offset bits.\n"                \
    "  -t <file>  Trace file.\n"                                 \
    "\n"                                                         \
    "Examples:\n"                                                \
    "  linux>  ./csim -s 4 -E 1 -b 4 -t traces/yi.trace\n"       \
    "  linux>  ./csim -v -s 8 -E 2 -b 4 -t traces/yi.trace"

描述

HELP_MESSAGE 是程序的帮助消息字符串,用于显示程序的使用方法和可用选项,以及一些示例用法。

Line 结构体

typedef struct Line {
    int valid;
    unsigned long tag;
    int timeStamp;
} Line;

描述

Line 结构体表示缓存中的一行。它包含了有效位、标记和时间戳等属性,用于表示缓存中的一行数据。

成员变量

  • valid:表示该行是否有效的标志。如果为 1,则表示该行有效;如果为 0,则表示该行无效。
  • tag:表示该行对应的标记值,用于标识缓存中存储的数据所属的地址范围。
  • timeStamp:表示该行的时间戳,用于记录该行最近一次被访问或修改的时间。

Set 结构体

typedef struct Set {
    Line *lines;
} Set;

描述

Set 结构体表示缓存中的一组。它包含了一组 Line 结构体的数组,用于表示缓存中的多行数据。

成员变量

  • lines:表示一组缓存行的数组。每个元素都是一个 Line 结构体,用于表示缓存中的一行数据。

Cache 结构体

struct Cache {
    int s;
    int E;
    int b;
    Set *sets;
} C;

描述

Cache 结构体表示整个缓存系统。它包含了缓存的一些基本参数以及一组 Set 结构体的数组,用于表示缓存中的多组数据。

成员变量

  • s:表示缓存的组索引位数。
  • E:表示每组的行数。
  • b:表示块偏移位数。
  • sets:表示缓存中的多组数据。每个元素都是一个 Set 结构体,用于表示缓存中的一组数据。

Function

initCache
void initCache(int s, int E, int b)

描述

初始化缓存,分配内存并设置初始值。

参数

  • s:缓存的组索引位数。
  • E:每组的行数。
  • b:块偏移位数。
verbosely
void verbosely(CheckFlag flag)

描述

根据给定的标志以详细模式输出缓存操作信息。

参数

  • flag:表示缓存操作类型的标志。
processFlag
void processFlag(CheckFlag flag)

描述

处理缓存操作的标志,更新相应的计数器。

参数

  • flag:表示缓存操作类型的标志。
getSetIndex
unsigned long getSetIndex(unsigned long address)

描述

根据给定的内存地址计算并返回相应的组索引。

参数

  • address:内存地址。

返回值

返回计算得到的组索引。

getTag
unsigned long getTag(unsigned long address)

描述

根据给定的内存地址计算并返回相应的标记。

参数

  • address:内存地址。

返回值

返回计算得到的标记。

processLoad
void processLoad(unsigned long address)

描述

处理加载操作。

参数

  • address:加载操作的内存地址。
processStore
void processStore(unsigned long address)

描述

处理存储操作,在模拟冲突逻辑上和processLoad一致。

参数

  • address:存储操作的内存地址。
processModify
void processModify(unsigned long address)

描述

处理修改操作,在模拟冲突上相当于一次processLoad操作和processStore操作。

参数

  • address:修改操作的内存地址。
processLine
void processLine(char *line)

描述

逻辑步骤:
解析操作类型、地址和大小:

从输入的字符串中解析出操作类型、内存地址和大小。
操作类型可以是加载(L)、存储(S)或修改(M)。
内存地址表示操作所涉及的内存地址。
大小表示操作所涉及的内存大小。
执行相应的操作:

根据解析得到的操作类型,调用相应的处理函数执行操作。

  • L: 调用 processLoad 函数处理加载操作。
  • S: 调用 processStore 函数处理存储操作。
  • M: 调用 processModify 函数处理修改操作。

参数

  • line:跟踪文件中的一行文本。
implement
void implement(int argc, char *argv[])

描述

实现主要逻辑,解析命令行参数并执行相应操作。

参数

  • argc:命令行参数数量。
  • argv:命令行参数数组。
main
int main(int argc, char *argv[])

描述

程序的入口点,调用 implement 函数执行主要逻辑。

参数

  • argc:命令行参数数量。
  • argv:命令行参数数组。

LRU

时间戳的定义:
int time = 0;
  • time 是一个全局变量,用于记录当前的时间戳值。
  • 初始值为 0,表示时间戳的初始时间。
更新时间戳的方法:
int newTimeStamp() {
    time++;
    return time;
}
  • newTimeStamp 函数用于更新时间戳的值,并返回更新后的时间戳。
  • 每次调用 newTimeStamp 函数,时间戳 time 的值会增加 1,表示时间的流逝。
  • 在缓存行被访问时,会调用 newTimeStamp 函数来更新该行的时间戳。
时间戳的使用:

在缓存行的结构体中,每个缓存行都有一个时间戳属性,用于记录该行最近一次被访问或修改的时间。在处理加载、存储和修改等操作时,会根据当前时间戳的值来更新相应缓存行的时间戳。

typedef struct Line {
    int valid;
    unsigned long tag;
    int timeStamp;
} Line;
逻辑:
  • 当加载、存储或修改操作需要访问缓存行时,会检查该行的时间戳,并将当前时间戳的值赋给该行。
  • 这样,可以通过比较时间戳来确定缓存行的访问顺序,从而实现最近最少使用(LRU)策略或其他替换策略。
  • 时间戳的更新保证了每次访问缓存行时都能更新该行的访问时间,从而维护了缓存行的访问顺序。

Part B

实验参数是$s=5,E=1,b=5$
给出的是一个直接映射缓存,有$2^5=32$组,每个块有$2^5=32$个字节,可以存$8$个int,也就是每次读取或存储会加载$8$个int大小的块
总共的存储大小合起来就是$32*8=256$个int

样例给出的暴力转置方式,对$B$数组以列模式进行访问,显然不满足良好的空间局部性
然而对于A数组的访问却是高效的one-step,在优化过程必须考虑综合A、B的访问时间

32x32

我们发现transpose这一操作不可避免地将$A$的行映射到$B$的列,如果简单地按“行-列”的访问模式对$A$进行操作,则一定会造成大量的访问冲突

考虑能否在连续访问$A$时是否能同时连续访问$B$,也就是每次尽量访问将$A$与$B$的不重叠的相对应部分

对矩阵进行分块。首先考虑不在对角线的情况,我们很容易想到一种分块方法:由于每个缓存块是$8$个int,我们干脆把块的大小设置成$8*8$。

对角线的话用临时变量一次读一行优化一下
实现一下:

for (int i = 0; i < N; i += 8) {
    for (int j = 0; j < M; j += 8) {
        for (int k = 0; k < 8; k++) {
            int tmp0 = A[i + k][j];
            int tmp1 = A[i + k][j + 1];
            int tmp2 = A[i + k][j + 2];
            int tmp3 = A[i + k][j + 3];
            int tmp4 = A[i + k][j + 4];
            int tmp5 = A[i + k][j + 5];
            int tmp6 = A[i + k][j + 6];
            int tmp7 = A[i + k][j + 7];
            B[j][i + k] = tmp0;
            B[j + 1][i + k] = tmp1;
            B[j + 2][i + k] = tmp2;
            B[j + 3][i + k] = tmp3;
            B[j + 4][i + k] = tmp4;
            B[j + 5][i + k] = tmp5;
            B[j + 6][i + k] = tmp6;
            B[j + 7][i + k] = tmp7;
        }
    }
}

得到不考虑对角线的情况:

Summary for official submission (func 0): correctness=1 misses=288

考虑继续分块,块大小必定小于$8$,因为一个地方最少要用两次(读,存),再乱搞misses肯定会增加,就不知道怎么搞了

参考了一下网上的博客,发现原来可以先把$A$复制到$B$的对应位置,然后再对$B$转置,这样行和列就不会冲突了,只需要读写一次
也就是:

#define swap(a, b) (a) ^= (b) ^= (a) ^= (b)
for (int i = 0; i < N; i += 8) {
    for (int j = 0; j < M; j += 8) {
        for (int k = 0; k < 8; k++) {
            int tmp0 = A[i + k][j];
            int tmp1 = A[i + k][j + 1];
            int tmp2 = A[i + k][j + 2];
            int tmp3 = A[i + k][j + 3];
            int tmp4 = A[i + k][j + 4];
            int tmp5 = A[i + k][j + 5];
            int tmp6 = A[i + k][j + 6];
            int tmp7 = A[i + k][j + 7];

            B[j + k][i] = tmp0;
            B[j + k][i + 1] = tmp1;
            B[j + k][i + 2] = tmp2;
            B[j + k][i + 3] = tmp3;
            B[j + k][i + 4] = tmp4;
            B[j + k][i + 5] = tmp5;
            B[j + k][i + 6] = tmp6;
            B[j + k][i + 7] = tmp7;
        }

        for (int k = 0; k < 8; k++) {
            for (int l = k + 1; l < 8; l++) {
                swap(B[j + k][i + l], B[j + l][i + k]);
            }
        }
    }
}

结果:

Summary for official submission (func 0): correctness=1 misses=260

64x64

这里扩大了一倍行和列,之前本来存一个$88$的块到这里不可行了,因为每次往下走一行的偏置$+8$,原来是$48=32$刚好不冲突用完所有组,这里$8*8=64$肯定会发生冲突了

如果缩小分块的大小,比如采用$4*4$的块,由于每次只存$4$个,最后的还要读取和写入的冲突因为步长短了一倍而多一倍,还是和之前一样

这里关键的一点是考虑一行一行的冲突,行冲突并不会造成一个块的冲突,于是可以考虑每次按$88$分块,然后按照$44$的块大小进行转移操作后再transpose

思路是对于两个对应的$8*8$的块,分别处理前四行和后四行,因为前四行的后四列对应的位置在后四行,所以要先暂时存在不对应的位置。
这时候有个问题,如何在转移前四行的后四列的同时,只造成一次不命中,这里就是之前提到的行冲突的关键。因为$A$的前四行和$B$在不对应的位置上,不会造成冲突,造成冲突的只可能是$B$中转移的操作,故可以先把转移操作的对应元素的值存下来,然后先更新前四行后四列的位置。再用存下来的值更新后四行的前四列。这样就只会造成一次冲突。

注意这里前四行的对应方式不再是把整个行存列,而是分块把行存列

for (int i = 0; i < N; i += 8) {
    for (int j = 0; j < M; j += 8) {
        for (int k = 0; k < 4; k++) {
            int tmp0 = A[i + k][j];
            int tmp1 = A[i + k][j + 1];
            int tmp2 = A[i + k][j + 2];
            int tmp3 = A[i + k][j + 3];
            int tmp4 = A[i + k][j + 4];
            int tmp5 = A[i + k][j + 5];
            int tmp6 = A[i + k][j + 6];
            int tmp7 = A[i + k][j + 7];

            B[j][i + k] = tmp0;
            B[j + 1][i + k] = tmp1;
            B[j + 2][i + k] = tmp2;
            B[j + 3][i + k] = tmp3;
            B[j][i + k + 4] = tmp4;
            B[j + 1][i + k + 4] = tmp5;
            B[j + 2][i + k + 4] = tmp6;
            B[j + 3][i + k + 4] = tmp7;
        }

        for (int k = 4; k < 8; k++) {
            int tmp0 = A[i + 4][j + k - 4];
            int tmp1 = A[i + 5][j + k - 4];
            int tmp2 = A[i + 6][j + k - 4];
            int tmp3 = A[i + 7][j + k - 4];
            int tmp4 = B[j + k - 4][i + 4];
            int tmp5 = B[j + k - 4][i + 5];
            int tmp6 = B[j + k - 4][i + 6];
            int tmp7 = B[j + k - 4][i + 7];

            B[j + k - 4][i + 4] = tmp0;
            B[j + k - 4][i + 5] = tmp1;
            B[j + k - 4][i + 6] = tmp2;
            B[j + k - 4][i + 7] = tmp3;

            B[j + k][i] = tmp4;
            B[j + k][i + 1] = tmp5;
            B[j + k][i + 2] = tmp6;
            B[j + k][i + 3] = tmp7;
        }

        for (int k = 4; k < 8; k++) {
            for (int l = 4; l < 8; l++) {
                B[j + k][i + l] = A[i + l][j + k];
            }
        }
    }
}

这里不考虑对角线的效率也很优秀了

结果:

Summary for official submission (func 0): correctness=1 misses=1220

61x67

这里因为行和列不能对齐块大小,所以计算非常麻烦,还是考虑分块,通过调节块的大小来找到最优解

块大小为$17$时可以通过

#define min(a, b) ((a) < (b) ? (a) : (b))
for (int i = 0; i < N; i += 17) {
    for (int j = 0; j < M; j += 17) {
        for (int k = 0; k < min(17, N - i); k++) {
            for (int l = 0; l < min(17, M - j); l++) {
                B[j + l][i + k] = A[i + k][j + l];
            }
        }
    }
}

最后

运行 ./driver.py,得到结果:

Cache Lab summary:
                        Points   Max pts      Misses
Csim correctness          27.0        27
Trans perf 32x32           8.0         8         260
Trans perf 64x64           8.0         8        1220
Trans perf 61x67          10.0        10        1951
          Total points    53.0        53


电波交流