Skip to content
This repository was archived by the owner on Apr 7, 2026. It is now read-only.

Commit 15ac7a4

Browse files
authored
Merge pull request #36 from ICS-25Fall-FDU/lab4-update
lab4 upd
2 parents d1e144a + fd1c416 commit 15ac7a4

2 files changed

Lines changed: 351 additions & 1 deletion

File tree

docs/.vitepress/config/zh.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,8 @@ function sidebarGuide(): DefaultTheme.Sidebar {
6060
{ text: 'Lab0: GitLab', link: '/lab/lab0' },
6161
{ text: 'Lab1: DataLab', link: '/lab/lab1' },
6262
{ text: 'Lab2: BombLab', link: '/lab/lab2' },
63-
{ text: 'Lab3: FlowLab', link: '/lab/lab3' }
63+
{ text: 'Lab3: FlowLab', link: '/lab/lab3' },
64+
{ text: 'Lab4: CacheLab', link: '/lab/lab4' }
6465
]
6566
},
6667
{

docs/lab/lab4.md

Lines changed: 349 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,349 @@
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

Comments
 (0)