考研408 计组第三章 存储器层次结构之Cache 20260903
(AI辅助生成)
408 计组里的 Cache,真正要掌握的不是背定义,而是把下面这条链完全打通:
为什么需要 Cache → Cache 怎么放数据 → CPU 如何判断命中 → 地址怎么拆 → 未命中怎么办 → 替换谁 → 写操作怎么处理 → 命中率/访问时间怎么算。
我按考研做题的逻辑从头讲。
一、Cache 到底是干什么的
CPU 很快,主存 DRAM 很慢。
例如可以粗略理解成:
- CPU 一次运算:几个 ns 甚至更短
- 主存访问:几十 ns
如果 CPU 每次取指令、读数据都等主存,CPU 大部分时间就在等。
所以在 CPU 和主存之间放一个:
容量小、速度快的存储器 Cache
结构:
CPU
↓
Cache
↓
主存
CPU 要访问某个地址时:
- 先查 Cache
- Cache 有 → 命中 Hit
- Cache 没有 → 缺失/未命中 Miss
- 再去主存取
- 把主存的一整块数据调入 Cache
- 再交给 CPU
二、为什么 Cache 有效:局部性原理
这是 Cache 最根本的理论基础。
1. 时间局部性
刚访问过的数据,很可能很快还会访问。
比如:
for (...) {
sum += a;
}
变量 sum 会反复访问。
所以:
刚访问过的数据留在 Cache 中,很可能马上再次命中。
2. 空间局部性
访问某个地址以后,很可能访问它附近的地址。
比如数组:
for (int i = 0; i < 1000; i++)
sum += a[i];
访问:
a[0]
a[1]
a[2]
a[3]
...
地址连续。
因此 Cache 不会只从主存取一个字节,而是:
一次取一个主存块 Block。
比如块大小 64B。
CPU 访问地址 1000 时,可能直接把:
960~1023
整块搬进 Cache。
之后访问附近地址就可以直接命中。
三、Cache 中最重要的单位:Cache 行
主存被划分为很多等大的:
主存块 Block
Cache 也划分成很多同样大小的:
Cache 行 Line
例如:
主存块大小 = Cache行大小 = 64B
注意:
主存容量和 Cache 容量当然不同,但它们的块大小必须相同。
例如:
主存:
块0
块1
块2
...
块1000000
Cache:
行0
行1
...
行255
主存有大量块,而 Cache 只能装其中少部分。
因此核心问题来了:
一个主存块应该放 Cache 的哪一行?
这就产生三种映射方式。
四、Cache 三种映射方式
408 极其重要:
- 直接映射
- 全相联映射
- 组相联映射
一定要理解三者本质。
五、直接映射
规则:
一个主存块只能放到 Cache 中唯一指定的一行。
公式:
例如 Cache 有 8 行。
那么:
| 主存块 | Cache 行 |
|---|---|
| 0 | 0 |
| 1 | 1 |
| 2 | 2 |
| ... | ... |
| 7 | 7 |
| 8 | 0 |
| 9 | 1 |
| 16 | 0 |
所以:
块0
块8
块16
块24
都只能放:
Cache行0
这就是直接映射的最大问题:
很容易发生冲突。
例如程序反复访问:
块0
块8
块0
块8
它们明明 Cache 总容量够,但因为都只能放行0:
块0进来
块8把块0挤掉
块0把块8挤掉
块8又把块0挤掉
疯狂 Miss。
这种称为:
冲突不命中 Conflict Miss
六、直接映射地址怎么拆
这是 408 计算题核心。
CPU 给的是:
主存地址
我们需要通过地址判断:
- Cache 哪一行
- 行里面哪个字节
- 当前 Cache 里面是不是我们想要的那个主存块
因此地址分成:
Tag | Index | Offset
标记 | 行号 | 块内地址
1. Offset:块内偏移
假设:
块大小 = 64B
因为:
所以:
这 6 位表示:
这一块里面第几个字节
2. Index:Cache 行号
假设 Cache 数据区容量:
16KB
块大小:
64B
则 Cache 行数:
所以:
3. Tag
假设主存地址:
32位
那么:
所以:
| Tag 18位 | Index 8位 | Offset 6位 |
七、为什么需要 Tag
这是很多人一开始没有真正理解的地方。
直接映射中:
主存块0
主存块256
主存块512
可能都映射到:
Cache行0
CPU 根据 Index 找到 Cache 行0以后,还不能确定:
里面到底是块0还是块256还是块512?
所以 Cache 行中必须额外保存:
Tag
例如:
Cache行0:
Valid = 1
Tag = 000101...
Data = ...
CPU 地址:
Tag | Index | Offset
先用 Index 找行:
Cache[Index]
然后比较:
CPU地址的Tag == Cache行里的Tag?
相等:
Hit
不相等:
Miss
八、Valid 位
Cache 刚开机的时候里面的数据没有意义。
所以每个 Cache 行通常有:
有效位 Valid
例如:
Valid = 0
表示:
这一行目前没有有效数据。
判断命中的完整逻辑:
Valid == 1
并且
Tag相等
才算:
Hit
所以:
九、全相联映射
全相联的规则是:
任意主存块,可以放 Cache 中任意一行。
因此没有固定:
块5 → 行5
而是:
块5 → Cache任意行
优点:
几乎没有映射冲突。
缺点:
CPU 查一个地址时不知道它在哪行。
因此必须:
把地址的 Tag 和 Cache 所有行 Tag 同时比较。
例如 256 行:
Tag → 比较器0
→ 比较器1
→ 比较器2
...
→ 比较器255
硬件成本非常高。
十、全相联地址拆分
没有 Index。
因为主存块可以放任何行。
所以地址只有:
| Tag | Offset |
例如:
32位地址
块大小 = 64B
Offset:
Tag:
所以:
| Tag 26位 | Offset 6位 |
十一、组相联映射
这是现代 Cache 最常见的方式,也是 408 最爱考的。
它是:
直接映射和全相联的折中。
先把 Cache 分成很多:
组 Set
每组里面有若干 Cache 行。
例如:
Cache共256行
4路组相联
意思是:
每组 4 行。
因此组数:
组相联的映射规则
主存块:
只能映射到某一个组,但可以放该组任意一行。
公式:
假设:
64组
4路
那么:
块0 → 组0
块64 → 组0
块128 → 组0
块192 → 组0
但组0里有:
4个位置
因此这四个块可以同时存在。
这就大幅降低了冲突。
十二、“几路组相联”到底是什么意思
这个概念一定不要背错。
N 路组相联 = 每组有 N 个 Cache 行。
例如:
2路组相联
组0:行0 行1
组1:行2 行3
...
每个主存块:
确定一个组
但:
组内2行任选一个。
4路组相联
每组:
4个Cache行
特殊情况
直接映射实际上就是:
1路组相联
因为一组只有一行。
全相联则可以理解成:
整个 Cache 只有一组。
这三个东西其实是一条连续谱:
1路
↓
直接映射
2路
4路
8路
...
↓
组相联
Cache总行数路
↓
全相联
十三、组相联地址怎么拆
还是:
| Tag | Set Index | Offset |
注意:
中间不是 Cache 行号,而是组号。
例如:
32位地址
Cache = 16KB
块大小 = 64B
4路组相联
第一步:Cache 行数
第二步:组数
所以:
组号 = 6位
第三步:块内地址
所以:
Offset = 6位
第四步:Tag
最终:
| Tag 20 | Set 6 | Offset 6 |
十四、三种映射一定这样总结
| 映射方式 | 主存块可以放哪里 | 地址 |
|---|---|---|
| 直接映射 | 唯一一行 | Tag + 行号 + Offset |
| 全相联 | 任意一行 | Tag + Offset |
| 组相联 | 唯一一组内任意一行 | Tag + 组号 + Offset |
硬件复杂度:
直接映射 < 组相联 < 全相联
冲突概率:
直接映射 > 组相联 > 全相联
命中速度通常:
直接映射最快
十五、Cache Miss 后发生什么
假设 CPU 访问:
地址 X
Cache 没命中。
则:
CPU
↓
Cache Miss
↓
访问主存
↓
主存找到包含X的整个块
↓
整个块调入Cache
↓
CPU获得需要的数据
注意一个经典考点:
CPU 虽然只访问一个字节/一个字,但 Cache 与主存之间通常传输的是一个完整块。
这是因为:
空间局部性。
十六、如果目标组已经满了怎么办
比如:
4路组相联
某组已经有:
A B C D
现在新块 E 也映射进这个组。
必须:
淘汰其中一个。
于是引出:
Cache 替换算法
十七、Cache 替换算法
408 常见:
- FIFO
- LRU
- Random
1. FIFO
First In First Out:
最早进入 Cache 的先被淘汰。
例如顺序:
A → B → C → D
现在 E 来:
淘汰A
FIFO 只关心:
谁来得最早。
不管最近是否访问过。
2. LRU
Least Recently Used:
淘汰最长时间没有被访问的块。
例如:
A B C D
近期访问顺序:
A
C
D
那么 B 很久没有访问。
新块进来:
淘汰B
LRU 利用了:
时间局部性。
即:
最近用过的数据未来更可能继续用。
所以通常比 FIFO 更合理。
3. Random
随机淘汰一个。
优点:
实现非常简单
实际硬件中有时候并不差。
十八、直接映射需要替换算法吗?
这是一个经典判断题。
答案:
不需要。
因为直接映射:
一个主存块只能放唯一一行
发生冲突时:
没有选择
直接覆盖。
所以没有:
“淘汰谁?”
这个问题。
而:
全相联
组相联
才需要替换算法。
十九、Cache 的写操作
前面讲的是:
CPU 读数据。
现在 CPU 要:
写数据
会出现一个麻烦:
Cache 中的数据变了,主存怎么办?
例如原本:
Cache中的X = 10
主存中的X = 10
CPU:
X = 20
Cache:
X = 20
主存可能仍然:
X = 10
于是产生一致性问题。
因此有两大写策略:
- 写直达 Write Through
- 写回 Write Back
二十、写直达 Write Through
CPU 写 Cache 的同时:
也立即写主存。
例如:
CPU
↓
Cache = 20
↓
主存 = 20
所以 Cache 和主存始终一致。
优点:
简单
一致性好
缺点:
每次写都访问主存
速度慢、总线流量大。
通常配合:
写缓冲 Write Buffer
CPU:
先把写请求放Write Buffer
然后继续运行。
Buffer 慢慢写主存。
二十一、写回 Write Back
CPU 写数据时:
只修改 Cache,不马上修改主存。
例如:
CPU写X=20
Cache:
X=20
主存:
X=10
直到:
这个 Cache 块将来要被淘汰
才把整块写回主存。
优点:
减少主存写次数
速度更快
缺点:
Cache和主存暂时不一致
控制复杂
因此需要增加一个:
Dirty Bit 脏位
二十二、Dirty 位
例如:
Dirty = 0
说明:
Cache 中的数据和主存一样,没有修改。
淘汰时:
直接扔掉
Dirty = 1
说明:
CPU 修改过 Cache,该块比主存新。
因此淘汰之前:
必须先写回主存
再加载新块。
所以写回 Cache 行常见结构:
Valid
Dirty
Tag
Data
二十三、写命中策略与写不命中策略要分开
408 很容易把它们混起来。
前面的:
Write Through
Write Back
是在讨论:
写命中以后怎么办。
但还有另一个问题:
CPU 要写的数据根本不在 Cache 中怎么办?
即:
Write Miss
这时有两种策略:
- 写分配 Write Allocate
- 非写分配 No Write Allocate
二十四、写分配 Write Allocate
如果:
Write Miss
则:
先把对应主存块调入 Cache,然后在 Cache 中修改。
流程:
CPU写X
↓
Cache没有X
↓
主存取X所在块
↓
加载进Cache
↓
修改X
一般和:
Write Back
搭配。
原因很好理解:
既然以后准备在 Cache 中反复修改,就先调进来。
二十五、非写分配 No Write Allocate
如果:
Write Miss
则:
不把块调入 Cache,直接写主存。
流程:
CPU写X
↓
Cache没有X
↓
直接写主存
一般和:
Write Through
搭配。
408 最经典组合记忆:
Write Back
+
Write Allocate
以及:
Write Through
+
No Write Allocate
但注意:
这是常见组合,不是逻辑上绝对只能这么组合。
二十六、Cache 命中率
假设总共访问:
1000次
其中:
950次命中
50次Miss
则命中率:
Miss Rate:
所以:
二十七、平均访问时间 AMAT
这是最重要的计算公式之一。
标准理解:
其中:
Hit Time
Cache 命中所需要的时间。
Miss Penalty
发生 Miss 后额外付出的代价。
例如:
Cache访问 = 1ns
Miss率 = 5%
Miss后访问主存额外需要 50ns
那么:
二十八、为什么有些题公式写法不一样?
有些教材写:
展开:
本质和 AMAT:
完全一样。
关键是:
看题目里的“主存访问时间”是否已经包含 Cache 查询时间。
不能机械套公式。
二十九、Cache 行总位数怎么算
408 很喜欢考:
“某 Cache 实际需要多少位存储?”
不要只计算 Data。
例如:
Cache数据容量 = 32KB
块大小 = 64B
32位地址
直接映射
写回
先求行数:
所以:
Index = 9位
Offset = 6位
Tag:
一行的数据位:
此外一行还要:
Tag = 17 bit
Valid = 1 bit
Dirty = 1 bit
所以每行总位数:
Cache 总位数:
注意:
“Cache 容量 32KB”通常说的是数据区容量,不包括 Tag、Valid、Dirty 等额外位。
如果题目问:
Cache 存储器实际总容量
才要把这些额外位算进去。
三十、块地址与字节地址
这一块特别容易出错。
假设:
主存按字节编址
块大小 = 64B
某地址:
0x1234
对应主存块号:
也就是:
块内偏移:
然后直接映射:
组相联:
这三个公式要非常熟。
三十一、举一个完整例子
假设:
32位地址
Cache数据容量 = 4KB
块大小 = 16B
2路组相联
求地址划分。
第一步:块内偏移
所以:
Offset = 4位
第二步:Cache 行数
第三步:组数
2 路,所以:
所以:
Set Index = 7位
第四步:Tag
所以最终:
| Tag 21 | Set 7 | Offset 4 |
这类题你以后最好固定按照:
块大小
↓
Offset
Cache容量÷块大小
↓
行数
行数÷路数
↓
组数
log2组数
↓
Index
总地址位数 - Index - Offset
↓
Tag
绝对不容易乱。
三十二、Cache Miss 的三种经典原因
常称为:
3C Miss
分别是:
1. Compulsory Miss
强制不命中 / 冷不命中。
某数据:
第一次访问
肯定不可能已经在 Cache 里。
例如:
第一次访问数组a
不可避免。
2. Capacity Miss
容量不命中。
程序工作集:
比Cache总容量还大
无论怎么映射都放不下。
3. Conflict Miss
冲突不命中。
Cache 总容量其实够,但因为:
映射位置限制
发生冲突。
主要出现在:
直接映射
组相联
全相联理论上:
没有 Conflict Miss。
三十三、增大 Cache 块大小有什么影响?
这是 408 很喜欢考的概念题。
刚开始增大块:
利用了空间局部性
一次把附近数据带进来。
所以:
Miss Rate可能下降
但是继续增大:
一个 Cache 能容纳的块数变少。
例如:
Cache = 4KB
块大小16B
→ 256块
块大小64B
→ 64块
块大小256B
→ 16块
于是:
冲突/容量问题增大
而且 Miss 一次需要传更多数据:
Miss Penalty增大
所以并不是:
块越大越好。
而是存在折中。
三十四、提高相联度的影响
例如:
直接映射
→ 2路
→ 4路
→ 8路
→ 全相联
优点:
冲突Miss减少
缺点:
需要同时比较更多Tag
硬件更复杂
命中时间可能增加
所以也不是:
相联度越高无脑越好。
三十五、多级 Cache
现代 CPU 一般不是只有一级:
CPU
↓
L1 Cache
↓
L2 Cache
↓
L3 Cache
↓
主存
通常:
| 层级 | 容量 | 速度 |
|---|---|---|
| L1 | 最小 | 最快 |
| L2 | 较大 | 较慢 |
| L3 | 更大 | 更慢 |
| DRAM | 最大 | 最慢 |
逻辑:
先查L1
↓ Miss
查L2
↓ Miss
查L3
↓ Miss
主存
三十六、多级 Cache 平均访问时间
假设:
L1命中时间 = T1
L1 Miss率 = m1
L2命中额外时间 = T2
L2局部Miss率 = m2
主存时间 = Tm
常见形式:
例如:
L1 = 1ns
L1 miss = 5%
L2 = 5ns
L2 miss = 10%
Memory = 100ns
则:
注意这里的:
L2 Miss率
通常指:
在已经 Miss L1 的访问中,又 Miss L2 的比例。
叫:
Local Miss Rate。
三十七、指令 Cache 和数据 Cache
CPU 访问主存有两类:
取指令
和:
读写数据
因此很多 CPU 的 L1 会分:
L1 I-Cache
Instruction Cache
以及:
L1 D-Cache
Data Cache。
这种设计叫:
分离 Cache。
优点:
CPU 可以同时:
取指令
+
访问数据
减少结构冲突。
较低级的 Cache 例如 L2/L3 常见统一 Cache。
三十八、Cache 和虚拟存储千万别混
这两个非常像,但目的不同。
Cache
解决:
CPU 和主存速度差距。
层次:
Cache ↔ 主存
单位:
Cache块
由:
硬件自动管理。
虚拟存储
解决:
主存容量不够 / 地址空间管理。
层次:
主存 ↔ 辅存
单位:
页面 Page
由:
硬件 + OS
共同管理。
但二者思想很像:
利用局部性
而且:
Cache块 ≠ 页
不要混。
三十九、Cache 和 TLB 也不要混
TLB 本质是:
页表项的 Cache。
虚拟地址:
VPN | Page Offset
先用 TLB 加速:
虚拟页号 → 物理页框号
得到物理地址。
然后物理地址再访问:
Cache
概念上:
TLB:
缓存地址翻译信息
Cache:
缓存真正的指令/数据
408 经常把:
TLB命中
Cache命中
Page Fault
混在一道题里。
要分清:
TLB Miss
≠
Cache Miss
≠
缺页
它们完全是三件不同的事。
四十、一道综合题应该怎么做
看到这种题:
某机器采用 32 位地址,按字节编址,数据 Cache 容量 32KB,块大小 64B,采用 4 路组相联……
你脑子里不要乱。
固定做:
第一步:Offset
所以:
6位
第二步:行数
第三步:组数
所以:
组号7位
第四步:Tag
所以:
Tag=19
最终:
| Tag 19 | Set 7 | Offset 6 |
四十一、如果题目给十六进制地址怎么办
例如:
地址 = 0x12345678
你不一定非得整个转成二进制。
如果字段边界刚好 4 bit 对齐,可以直接按十六进制切。
因为:
1个十六进制位 = 4 bit
例如块大小:
16B = 2^4
那么最低:
1个十六进制位
就是 Offset。
如果:
Index = 8bit
就是:
2个十六进制位
这样考试会快很多。
如果不是 4 的倍数,再转局部二进制即可。
四十二、408 最容易错的 10 个点
你做题时尤其注意这些。
1. Cache 容量通常指 Data 容量
不是:
Data + Tag + Valid
除非题目明确说:
Cache 总存储容量。
2. N 路组相联
意思是:
每组N行
不是:
有N组。
这个非常高频。
3. Index 位数由“组数”决定
组相联:
不是行数。
4. Offset 由块大小决定
如果按字节编址:
5. 全相联没有 Index
地址:
Tag + Offset
6. 直接映射不需要替换算法
因为:
替换位置唯一。
7. 写回需要 Dirty Bit
写直达:
一般不需要脏位。
8. Hit 要同时满足
Valid=1
+
Tag匹配
不是只看 Tag。
9. Cache Miss 不等于缺页
Cache Miss:
去主存
缺页:
去磁盘/辅存
代价差几个数量级。
10. Cache 调入的是整块
不是只调 CPU 当前要的那一个字节。
四十三、建议你把 Cache 体系压缩成这一张脑图
你最终考试脑子里只需要有:
Cache
│
┌────────────────┼─────────────────┐
│ │ │
为什么 怎么找 Miss怎么办
│ │ │
局部性 映射方式 调主存
│ │ │ │ │ │
时间 空间 直接 组相联 全相联 替换
│ │ │
Tag Index Offset FIFO/LRU
│
│
写
┌─────┴─────┐
写直达 写回
│
Dirty
而所有计算题,本质都绕不开下面四个公式:
最后:
其中直接映射:
路数=1
全相联:
组数=1
Index=0
所以三种映射其实可以被这一套公式完全统一。
最后给你一个非常重要的做题视角
你以后看到 Cache 题,先不要想“直接映射、组相联这些定义”。
先问自己三个问题:
① 一个块有多大?
决定 Offset。
② 这个主存块能去哪几个位置?
决定映射方式、组号和相联度。
③ 找到位置以后,怎么证明里面真的是我要的块?
靠 Tag + Valid。
如果这三个问题彻底理解,408 Cache 绝大多数地址划分、命中判断、替换、容量计算题其实都会变成机械计算。

浙公网安备 33010602011771号