|
| 1 | +# Lab4: Cachelab |
| 2 | + |
| 3 | +> Deadline: 2025-12-11 23:59:59 |
| 4 | +
|
| 5 | +## 实验概述 |
| 6 | + |
| 7 | +本实验为 CSAPP 第 6 章配套实验,目的是加深同学们对高速缓存 cache 的认识。实验分为三个部分: |
| 8 | + |
| 9 | +- **Part A**:用 **C语言** 实现一个 cache 模拟器,使其能读取指定格式的 trace文件,并且输出命中、缺失、替换次数。我们已经为你提供一部分的代码。 |
| 10 | +- **Part B**:根据特定的 cache 参数设计一个矩阵转置的算法,使得矩阵转置运算中 cache 的 miss 次数尽可能低。 |
| 11 | +- **Honor Part**:实现矩阵乘法的 cache 友好版本。 |
| 12 | + |
| 13 | +考虑到PJ将至,助教将本次lab的难度相较于原版调低了一些(除了Honor Part,但Honor Part的分数很少),而且本次实验全程用 C语言(可以不用和抽象的汇编打交道了),所以大家不用过于担心~ |
| 14 | + |
| 15 | +分值分配: |
| 16 | +- Part A: 40% |
| 17 | +- Part B: 45% |
| 18 | +- Honor Part: 10% |
| 19 | +- 实验报告 + 代码风格: 15% |
| 20 | + |
| 21 | +## 部署实验环境 |
| 22 | + |
| 23 | +> [!important] |
| 24 | +> |
| 25 | +> 点击 [此链接](https://classroom.github.com/a/lYaB96qv) 领取作业 |
| 26 | +
|
| 27 | +### 准备工作 |
| 28 | + |
| 29 | +- **gcc**:`gcc -v`,如未安装:`sudo apt-get install gcc` |
| 30 | +- **make**:`make -v`,如未安装:`sudo apt-get update && sudo apt-get install make libc6 libc6-dev libc6-dev-i386` |
| 31 | +- **python**:`python --version`(一般已有) |
| 32 | +- **valgrind**:`sudo apt-get install valgrind` |
| 33 | + |
| 34 | +## Part A |
| 35 | + |
| 36 | +设计一个 cache 模拟器,读入指定格式的 trace 文件,模拟 cache 的运行过程,然后输出 cache 的命中、缺失、替换次数。trace 文件是通过 valgrind 的 lackey 工具生成的,它具有以下格式: |
| 37 | + |
| 38 | +``` |
| 39 | +I 0400d7d4,8 |
| 40 | + M 0421c7f0,4 |
| 41 | + L 04f6b868,8 |
| 42 | + S 7ff0005c8,8 |
| 43 | +``` |
| 44 | + |
| 45 | +每行格式为 : |
| 46 | + |
| 47 | +``` |
| 48 | +[space]operation address,size |
| 49 | +``` |
| 50 | + |
| 51 | +其中`I`代表读指令操作,`L`代表读数据操作,`S`代表写数据操作,`M`代表修改数据操作(即读数据后写数据)。除 |
| 52 | +了`I`操作外,其他操作都会在开头都会有一个空格。`address`为操作的地址,`size`为操作的大小(单位为字节)。 |
| 53 | + |
| 54 | +你的所有实现都在 `csim.c` / `csim.h`中。你的全局变量和函数需要定义在`csim.h`中,你的函数实现需要在`csim.c`中。 |
| 55 | +我们在`ref-bin`文件夹下提供了一个`csim-ref`文件,这是一个参考实现,你可以通过它来检查你的实现是否正确,它的用法如下: |
| 56 | + |
| 57 | +```bash |
| 58 | +./ref-bin/csim-ref [-hv] -s <s> -E <E> -b <b> -t <tracefile> |
| 59 | +``` |
| 60 | +- `-h` 代表帮助 |
| 61 | +- `-v` 代表 verbose, 即输出详细信息 |
| 62 | +- `-s` 代表 cache set index 的位数 (实际 cache 的 set 数 为 `2^s`) |
| 63 | +- `-E` 代表每个 set 中的 cache line 的个数 |
| 64 | +- `-b` 代表块偏移位数(块大小 `B=2^b`) |
| 65 | +- `-t` 代表trace文件的路径 |
| 66 | + |
| 67 | +`csim-ref`会输出 cache 的命中、缺失、替换次数,比如: |
| 68 | + |
| 69 | +```bash |
| 70 | +$ ./ref-bin/csim-ref -s 16 -E 1 -b 16 -t traces/yi.trace |
| 71 | +hits:8 misses:1 evictions:0 |
| 72 | + |
| 73 | +$ ./ref-bin/csim-ref -v -s 16 -E 1 -b 16 -t traces/yi.trace |
| 74 | +L 10,1 miss |
| 75 | +M 20,1 hit hit |
| 76 | +L 22,1 hit |
| 77 | +S 18,1 hit |
| 78 | +L 110,1 hit |
| 79 | +L 210,1 hit |
| 80 | +M 12,1 hit hit |
| 81 | +hits:8 misses:1 evictions:0 |
| 82 | +``` |
| 83 | + |
| 84 | +> [!NOTE] |
| 85 | +> |
| 86 | +> **Task 1 (40 pts)** |
| 87 | +> |
| 88 | +> 你的实现需要具有和`csim-ref`相同的功能,包括`verbose`模式输出debug信息。 |
| 89 | +> 在`csim.c`中,我们已经为你提供了基本的解析命令行参数的代码,你需要在此基础上进行实现。 |
| 90 | +> |
| 91 | +> **需求:** |
| 92 | +> |
| 93 | +> 1. 你的代码在编译时不能存在warning。 |
| 94 | +> 2. 你只能使用 **c语言** 来实现。 |
| 95 | +> 3. 虽然给了测试数据,但不允许面向数据编程,助教会做源码检查;不允许通过直接调用`csim-ref`来实现。 |
| 96 | +
|
| 97 | +每次修改你的代码,在进行测试前先编译: |
| 98 | + |
| 99 | +```bash |
| 100 | +$ make clean && make # 生成 ./csim |
| 101 | +``` |
| 102 | + |
| 103 | +共有8项测试 |
| 104 | + |
| 105 | +```bash |
| 106 | +$ ./csim -s 1 -E 1 -b 1 -t traces/yi2.trace |
| 107 | +$ ./csim -s 4 -E 2 -b 4 -t traces/yi.trace |
| 108 | +$ ./csim -s 2 -E 1 -b 4 -t traces/dave.trace |
| 109 | +$ ./csim -s 2 -E 1 -b 3 -t traces/trans.trace |
| 110 | +$ ./csim -s 2 -E 2 -b 3 -t traces/trans.trace |
| 111 | +$ ./csim -s 2 -E 4 -b 3 -t traces/trans.trace |
| 112 | +$ ./csim -s 5 -E 1 -b 5 -t traces/trans.trace |
| 113 | +$ ./csim -s 5 -E 1 -b 5 -t traces/long.trace |
| 114 | +``` |
| 115 | + |
| 116 | +原始分为:前7项每项3分,最后一项6分,共27分。对于每一项,hit、miss、eviction的正确性各占 1/3 的分数。 |
| 117 | + |
| 118 | +原始分会被乘以 40 / 27 得到最终的分数。 |
| 119 | + |
| 120 | +***最终的分数可以直接通过`python3 driver.py`来查看。*** |
| 121 | + |
| 122 | +### hints |
| 123 | + |
| 124 | +- 可以使用`malloc`和`free`构造 cache。 |
| 125 | +- 你可以使用`csim-ref`来检查你的实现是否正确,通过开启`verbose`模式可以更好地debug。 |
| 126 | +- LRU算法可以简单地使用计数器的实现方式。 |
| 127 | + |
| 128 | +## Part B |
| 129 | + |
| 130 | +cache为何被称为“高速缓存”,是因为读取cache的速率远快于读取主存的速率(可能大概100倍),因此cache miss的次数往往决定了程序的运行速度。因此,我们需要尽可能设计cache-friendly的程序,使得cache miss的次数尽可能少。 |
| 131 | + |
| 132 | +在这部分的实验,你将对矩阵转置程序(一个非常容易cache miss的程序)进行优化,让cache miss的次数尽可能少。你的分数将由cache miss的次数决定。 |
| 133 | + |
| 134 | +> [!NOTE] |
| 135 | +> |
| 136 | +> **Task 2.1 (36 pts)** |
| 137 | +> |
| 138 | +> 你的所有实现都将在`trans.c`中。你将设计这样的一个函数:它接收四个参数:M,N,一个N * M的矩阵A和一个M * N的矩阵B,你需要把A转置后的结果存入B中。 |
| 139 | +> |
| 140 | +
|
| 141 | +```c |
| 142 | +char trans_desc[] = "some description"; |
| 143 | +void trans(int M, int N, int A[N][M], int B[M][N]) |
| 144 | +{ |
| 145 | + // your implementation here |
| 146 | +} |
| 147 | +``` |
| 148 | +
|
| 149 | +每设计好一个这样的函数,你都可以在`registerFunctions()`中为其进行“注册”,只有“注册”了的函数才会被加入之后的评测中,你可以“注册”并评测多个函数;为上面的函数进行注册只需要将下面代码加入`registerFunctions()`中 |
| 150 | +
|
| 151 | +```c |
| 152 | +registerTransFunction(trans, trans_desc); |
| 153 | +``` |
| 154 | + |
| 155 | +我们提供了一个名为`trans()`的函数作为示例。 |
| 156 | + |
| 157 | +你需要保证有一个且有唯一一个“注册”的函数用于最终提交,我们将靠“注册”时的description进行区分,请确保你的提交函数的description是“Transpose submission” ,比如: |
| 158 | + |
| 159 | +```c |
| 160 | +char transpose_submit_desc[] = "Transpose submission"; |
| 161 | +void transpose_submit(int M, int N, int A[N][M], int B[M][N]) |
| 162 | +{ |
| 163 | + // your implementation here |
| 164 | +} |
| 165 | +``` |
| 166 | +
|
| 167 | +我们将使用特定形状的矩阵和特定参数的cache来进行评测,所以你 **可以** 针对这些特殊情况来编写代码。 |
| 168 | +
|
| 169 | +**要求**: |
| 170 | +
|
| 171 | +- 你的代码在编译时不能存在warning。 |
| 172 | +- 在每个矩阵转置函数中,你至多能定义12个int类型的局部变量(不包括循环变量,但你不能将循环变量用作其他用途),且不能使用任何全局变量。你不能定义除int以外类型的变量。你不能使用malloc等方式申请内存块。你可以使用int数组,但等同于数组大小的数量的int类型变量也同样被计入。 |
| 173 | +- 你不能使用递归。 |
| 174 | +- 你只允许使用一个函数完成矩阵转置的功能,而不能在函数中调用任何辅助函数。 |
| 175 | +- 你不能修改原始的矩阵A,但是你可以任意修改矩阵B。 |
| 176 | +- 你可以定义宏。 |
| 177 | +
|
| 178 | +**评分**: |
| 179 | +
|
| 180 | +我们将使用cache参数为:`S = 48, E = 1, B = 48`。 |
| 181 | +我们将使用以下3种矩阵来进行评测: |
| 182 | +
|
| 183 | + - 48 * 48的矩阵,分值`12`分,miss次数`< 500`则满分,miss次数`> 800`则0分,`500~800`将按miss次数获取一定比例的分数。 |
| 184 | + - 96 * 96的矩阵,分值`12`分,miss次数`< 2200`则满分,miss次数`> 3000`则0分,`2200~3000`将按miss次数获取一定比例的分数。 |
| 185 | + - 93 * 99的矩阵,分值`12`分,miss次数`< 3000`则满分,miss次数`> 4000`则0分,`3000~4000`将按miss次数获取一定比例的分数。 |
| 186 | +
|
| 187 | +我们只会针对这三种矩阵进行测试,所以你 **可以** 只考虑这三种情况。 |
| 188 | +
|
| 189 | +#### step 0 |
| 190 | +
|
| 191 | +```bash |
| 192 | +make clean && make |
| 193 | +``` |
| 194 | + |
| 195 | +#### step 1 |
| 196 | + |
| 197 | +在测试之前,进行算法正确性的测试。 |
| 198 | + |
| 199 | +```bash |
| 200 | +./tracegen -M <row> -N <col> |
| 201 | +``` |
| 202 | + |
| 203 | +比如对48 * 48转置函数进行测试。 |
| 204 | + |
| 205 | +```bash |
| 206 | +./tracegen -M 48 -N 48 |
| 207 | +``` |
| 208 | + |
| 209 | +你也可以对特定的函数进行测试,比如对第0个“注册”的函数。 |
| 210 | + |
| 211 | +```bash |
| 212 | +./tracegen -M 48 -N 48 -F 0 |
| 213 | +``` |
| 214 | + |
| 215 | +#### step 2 |
| 216 | + |
| 217 | +```bash |
| 218 | +./test-trans -M <row> -N <col> |
| 219 | +``` |
| 220 | + |
| 221 | +这个程序将使用valgrind工具生成trace文件,然后调用csim-ref程序获取cache命中、缺失、替换的次数。 |
| 222 | + |
| 223 | +### hints |
| 224 | + |
| 225 | +- 在调用`./test-trans`之后,可以使用如下命令查看你的cache命中/缺失情况;你可以把`f0`替换为`fi`来查看第 i 个“注册”的函数带来的cache命中/缺失情况。 |
| 226 | + |
| 227 | +```bash |
| 228 | +./csim-ref -v -S 48 -E 1 -B 48 -t trace.f0 > result.txt |
| 229 | +``` |
| 230 | + |
| 231 | +- [这篇文章可能对你有所启发](http://csapp.cs.cmu.edu/public/waside/waside-blocking.pdf) |
| 232 | +- 你可能要考虑冲突带来的miss。 |
| 233 | +- 脑测一下你的miss次数或许是一个很好的选择,你可以计算一下大概有多少比例的miss,然后乘以总的读写次数;你可以在上面生成的`result.txt`文件中验证你的想法。 |
| 234 | +- 你可以认为A和B矩阵的起始地址位于某个cacheline的开始(即A和B二维数组的起始地址能被48整除)。 |
| 235 | + |
| 236 | +### 矩阵转置优化 Plus |
| 237 | + |
| 238 | +> [!NOTE] |
| 239 | +> **Task 2.2 (9 pts)** |
| 240 | +> 1. 将 Part B 的 48 * 48 矩阵转置的 cache miss 优化到 **450 次以下** (4分) |
| 241 | +> 2. 将 Part B 的 96 * 96 矩阵转置的 cache miss 优化到 **1900 次以下** (5分) |
| 242 | +
|
| 243 | +## Honor Part |
| 244 | + |
| 245 | +恭喜你!你已经来到了 Cachelab 的**最后一部分**。 |
| 246 | + |
| 247 | +在完成了矩阵转置的优化后,我们来研究一个更具挑战性的问题:**矩阵乘法的缓存优化**。 |
| 248 | + |
| 249 | +```c |
| 250 | +for (int i = 0; i < N; i++) { |
| 251 | + for (int j = 0; j < N; j++) { |
| 252 | + C[i][j] = 0; |
| 253 | + for (int k = 0; k < M; k++) { |
| 254 | + C[i][j] += A[i][k] * B[k][j]; |
| 255 | + } |
| 256 | + } |
| 257 | +} |
| 258 | +``` |
| 259 | + |
| 260 | +这段代码是实现矩阵乘法的最朴素算法,虽然看起来很简洁优雅,但从缓存角度分析,存在严重的性能问题。我们不妨分析一下是哪里出现了问题。 |
| 261 | + |
| 262 | +假设矩阵按行优先存储,我们来分析一下内循环(`k` 循环)中的访问模式: |
| 263 | + |
| 264 | + 对于`C[i][j]`,每次迭代访问同一地址,编译器通常会用寄存器优化。 |
| 265 | + |
| 266 | + 对于`A[i][k]`,随着 `k` 增加,沿着 A 的第 `i` 行连续访问。 |
| 267 | + |
| 268 | + 对于`B[k][j]`,每次 `k` 增加 1,需要跳过整整一行,相当于**按列访问**矩阵 B。 |
| 269 | + |
| 270 | +显然,在这里`B[k][j]`的读取导致了**大量的 cache miss**。由于按列访问,虽然每次加载一个 cache line ,但只使用其中一个元素就跳到下一行,缓存利用率很低。我们或许可以考虑更改循环顺序(比如把 k 调到中间那一层循环?)但是即使改变了循环顺序,当矩阵较大时,整个矩阵仍然无法完全放入缓存,还是会产生**大量的 cache miss**。所以我们可以考虑将大矩阵分解为小块,每次只处理能装入缓存的小块,这也就是分块算法的原理。 |
| 271 | + |
| 272 | +对于 $C = A \times B$,我们可以将矩阵分块: |
| 273 | + |
| 274 | +$$ |
| 275 | +\begin{bmatrix} |
| 276 | +C_{11} & C_{12} \\ |
| 277 | +C_{21} & C_{22} |
| 278 | +\end{bmatrix} = |
| 279 | +\begin{bmatrix} |
| 280 | +A_{11} & A_{12} \\ |
| 281 | +A_{21} & A_{22} |
| 282 | +\end{bmatrix} \times |
| 283 | +\begin{bmatrix} |
| 284 | +B_{11} & B_{12} \\ |
| 285 | +B_{21} & B_{22} |
| 286 | +\end{bmatrix} |
| 287 | +$$ |
| 288 | + |
| 289 | +其中每个子块的计算为: |
| 290 | +$C_{ij} = \sum_{k} A_{ik} \times B_{kj}$ |
| 291 | +当然,这里提出的分块算法只是抛砖引玉,想要通过Honor Part还需在此基础上继续思考和优化。大家八仙过海,各显神通吧! |
| 292 | + |
| 293 | +#### 测试环境 |
| 294 | + |
| 295 | +**矩阵规模**:$A_{32 \times 32}$, $B_{32 \times 32}$,计算 $C = A \times B$ |
| 296 | + |
| 297 | +**缓存配置**: |
| 298 | + |
| 299 | +- 组数 S = 32 |
| 300 | +- 相连度 E = 1 |
| 301 | +- 块大小 B = 32 |
| 302 | + |
| 303 | +> [!NOTE] |
| 304 | +> **Task 3 (10 pts)** |
| 305 | +> |
| 306 | +> 在 `honor-part/mul.c` 的 `mul_submit` 函数中实现矩阵乘法算法,要求 **cache miss < 4000**。 |
| 307 | +> |
| 308 | +> 需求(同Part B): |
| 309 | +> |
| 310 | +> 1. 最多定义 **16 个 int 类型**的局部变量(循环变量不计入,但不能将循环变量用作其他用途)。 |
| 311 | +> 2. 不能修改矩阵 A 和 B,但可以任意修改矩阵C。 |
| 312 | +
|
| 313 | +#### 测试方法 |
| 314 | + |
| 315 | +```bash |
| 316 | +cd honor-part |
| 317 | +make clean && make |
| 318 | +./build/bin/test-mul -M 32 -N 32 |
| 319 | +``` |
| 320 | + |
| 321 | +输出示例: |
| 322 | + |
| 323 | +```bash |
| 324 | +Function 0 (1 total) |
| 325 | +Step 1: Validating and generating memory traces |
| 326 | +Step 2: Evaluating performance (s=32, E=1, b=32) |
| 327 | +func 0 (multiply submission): hits:XXXX, misses:YYYY, evictions:ZZZZ |
| 328 | + |
| 329 | +Summary for official submission (func 0): correctness=1 misses=YYYY |
| 330 | +TEST_MUL_RESULTS=1:YYYY |
| 331 | +``` |
| 332 | + |
| 333 | +## 提交 |
| 334 | + |
| 335 | +实验报告中可以包括下面内容: |
| 336 | + |
| 337 | +- 代码运行效果展示 |
| 338 | +- 实现思路和创新点 |
| 339 | +- 对后续实验的建议 |
| 340 | +- 其他任何你想写的内容 |
| 341 | + |
| 342 | +实验报告及代码通过 Github 提交。 |
| 343 | + |
| 344 | +## 参考资料 |
| 345 | + |
| 346 | +- CMU 原版 Lab: http://csapp.cs.cmu.edu/3e/labs.html |
| 347 | +- 本实验参考 2023、2024 年的 Cachelab 实验开发 |
| 348 | + |
| 349 | +> 负责助教: 项正豪 @[xzh2004](https://github.com/xzh2004) 蔡亦扬 @[Caibao7](https://github.com/Caibao7) |
0 commit comments