考研408 计组第三章 存储器层次结构之Cache 20260903

(AI辅助生成)
408 计组里的 Cache,真正要掌握的不是背定义,而是把下面这条链完全打通:

为什么需要 Cache → Cache 怎么放数据 → CPU 如何判断命中 → 地址怎么拆 → 未命中怎么办 → 替换谁 → 写操作怎么处理 → 命中率/访问时间怎么算。

我按考研做题的逻辑从头讲。


一、Cache 到底是干什么的

CPU 很快,主存 DRAM 很慢。

例如可以粗略理解成:

  • CPU 一次运算:几个 ns 甚至更短
  • 主存访问:几十 ns

如果 CPU 每次取指令、读数据都等主存,CPU 大部分时间就在等。

所以在 CPU 和主存之间放一个:

容量小、速度快的存储器 Cache

结构:

CPU
 ↓
Cache
 ↓
主存

CPU 要访问某个地址时:

  1. 先查 Cache
  2. Cache 有 → 命中 Hit
  3. Cache 没有 → 缺失/未命中 Miss
  4. 再去主存取
  5. 把主存的一整块数据调入 Cache
  6. 再交给 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 极其重要:

  1. 直接映射
  2. 全相联映射
  3. 组相联映射

一定要理解三者本质。


五、直接映射

规则:

一个主存块只能放到 Cache 中唯一指定的一行。

公式:

\[Cache行号 = 主存块号 \bmod 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 给的是:

主存地址

我们需要通过地址判断:

  1. Cache 哪一行
  2. 行里面哪个字节
  3. 当前 Cache 里面是不是我们想要的那个主存块

因此地址分成:

Tag | Index | Offset
标记 | 行号 | 块内地址

1. Offset:块内偏移

假设:

块大小 = 64B

因为:

\[64=2^6 \]

所以:

\[Offset=6位 \]

这 6 位表示:

这一块里面第几个字节

2. Index:Cache 行号

假设 Cache 数据区容量:

16KB

块大小:

64B

则 Cache 行数:

\[\frac{16KB}{64B} = \frac{2^{14}}{2^6} = 2^8 = 256 \]

所以:

\[Index=8位 \]


3. Tag

假设主存地址:

32位

那么:

\[Tag位数 = 32-8-6 = 18 \]

所以:

| 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

所以:

\[Hit = Valid \land TagMatch \]


九、全相联映射

全相联的规则是:

任意主存块,可以放 Cache 中任意一行。

因此没有固定:

块5 → 行5

而是:

块5 → Cache任意行

优点:

几乎没有映射冲突。

缺点:

CPU 查一个地址时不知道它在哪行。

因此必须:

把地址的 Tag 和 Cache 所有行 Tag 同时比较。

例如 256 行:

Tag → 比较器0
    → 比较器1
    → 比较器2
    ...
    → 比较器255

硬件成本非常高。


十、全相联地址拆分

没有 Index。

因为主存块可以放任何行。

所以地址只有:

| Tag | Offset |

例如:

32位地址
块大小 = 64B

Offset:

\[6位 \]

Tag:

\[32-6=26位 \]

所以:

| Tag 26位 | Offset 6位 |

十一、组相联映射

这是现代 Cache 最常见的方式,也是 408 最爱考的。

它是:

直接映射和全相联的折中。

先把 Cache 分成很多:

组 Set

每组里面有若干 Cache 行。

例如:

Cache共256行
4路组相联

意思是:

每组 4 行。

因此组数:

\[256 / 4 = 64组 \]


组相联的映射规则

主存块:

只能映射到某一个组,但可以放该组任意一行。

公式:

\[Cache组号 = 主存块号 \bmod Cache组数 \]

假设:

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 行数

\[16KB / 64B = 256行 \]

第二步:组数

\[256 / 4 = 64组 = 2^6 \]

所以:

组号 = 6位

第三步:块内地址

\[64B=2^6B \]

所以:

Offset = 6位

第四步:Tag

\[32-6-6 = 20位 \]

最终:

| 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 常见:

  1. FIFO
  2. LRU
  3. 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

于是产生一致性问题。

因此有两大写策略:

  1. 写直达 Write Through
  2. 写回 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

这时有两种策略:

  1. 写分配 Write Allocate
  2. 非写分配 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

则命中率:

\[H=\frac{950}{1000}=95\% \]

Miss Rate:

\[1-H \]

所以:

\[MissRate=5\% \]


二十七、平均访问时间 AMAT

这是最重要的计算公式之一。

标准理解:

\[AMAT = HitTime + MissRate\times MissPenalty \]

其中:

Hit Time

Cache 命中所需要的时间。

Miss Penalty

发生 Miss 后额外付出的代价。

例如:

Cache访问 = 1ns
Miss率 = 5%
Miss后访问主存额外需要 50ns

那么:

\[AMAT = 1+0.05\times50 = 3.5ns \]


二十八、为什么有些题公式写法不一样?

有些教材写:

\[T_a = H T_c+(1-H)(T_c+T_m) \]

展开:

\[T_a = T_c+(1-H)T_m \]

本质和 AMAT:

\[HitTime+MissRate\times MissPenalty \]

完全一样。

关键是:

看题目里的“主存访问时间”是否已经包含 Cache 查询时间。

不能机械套公式。


二十九、Cache 行总位数怎么算

408 很喜欢考:

“某 Cache 实际需要多少位存储?”

不要只计算 Data。

例如:

Cache数据容量 = 32KB
块大小 = 64B
32位地址
直接映射
写回

先求行数:

\[32KB/64B = 512行 = 2^9 \]

所以:

Index = 9位
Offset = 6位

Tag:

\[32-9-6=17位 \]

一行的数据位:

\[64B=512bit \]

此外一行还要:

Tag = 17 bit
Valid = 1 bit
Dirty = 1 bit

所以每行总位数:

\[512+17+1+1=531bit \]

Cache 总位数:

\[531\times512 \]

注意:

“Cache 容量 32KB”通常说的是数据区容量,不包括 Tag、Valid、Dirty 等额外位。

如果题目问:

Cache 存储器实际总容量

才要把这些额外位算进去。


三十、块地址与字节地址

这一块特别容易出错。

假设:

主存按字节编址
块大小 = 64B

某地址:

0x1234

对应主存块号:

\[\left\lfloor \frac{地址}{64}\right\rfloor \]

也就是:

\[BlockNumber=Address / BlockSize \]

块内偏移:

\[Offset=Address\bmod BlockSize \]

然后直接映射:

\[CacheLine = BlockNumber\bmod CacheLineCount \]

组相联:

\[Set = BlockNumber\bmod SetCount \]

这三个公式要非常熟。


三十一、举一个完整例子

假设:

32位地址
Cache数据容量 = 4KB
块大小 = 16B
2路组相联

求地址划分。

第一步:块内偏移

\[16B=2^4 \]

所以:

Offset = 4位

第二步:Cache 行数

\[4KB / 16B = 4096/16 = 256行 \]


第三步:组数

2 路,所以:

\[256/2=128组 \]

\[128=2^7 \]

所以:

Set Index = 7位

第四步:Tag

\[32-7-4 = 21 \]

所以最终:

| 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

常见形式:

\[AMAT = T_1+ m_1(T_2+m_2T_m) \]

例如:

L1 = 1ns
L1 miss = 5%

L2 = 5ns
L2 miss = 10%

Memory = 100ns

则:

\[AMAT = 1+ 0.05(5+0.1\times100) \]

\[= 1+0.05\times15 = 1.75ns \]

注意这里的:

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

\[64B=2^6 \]

所以:

6位

第二步:行数

\[32KB/64B = 512行 \]

第三步:组数

\[512/4 = 128组 = 2^7 \]

所以:

组号7位

第四步:Tag

\[32-7-6=19 \]

所以:

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 位数由“组数”决定

组相联:

\[Index=\log_2组数 \]

不是行数。


4. Offset 由块大小决定

如果按字节编址:

\[Offset=\log_2块大小(B) \]


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

而所有计算题,本质都绕不开下面四个公式:

\[\boxed{Cache行数=\frac{Cache数据容量}{块大小}} \]

\[\boxed{组数=\frac{Cache行数}{路数}} \]

\[\boxed{Offset位数=\log_2块大小} \]

\[\boxed{Index位数=\log_2组数} \]

最后:

\[\boxed{Tag=地址总位数-Index-Offset} \]

其中直接映射:

路数=1

全相联:

组数=1
Index=0

所以三种映射其实可以被这一套公式完全统一。


最后给你一个非常重要的做题视角

你以后看到 Cache 题,先不要想“直接映射、组相联这些定义”。

先问自己三个问题:

① 一个块有多大?
决定 Offset。

② 这个主存块能去哪几个位置?
决定映射方式、组号和相联度。

③ 找到位置以后,怎么证明里面真的是我要的块?
靠 Tag + Valid。

如果这三个问题彻底理解,408 Cache 绝大多数地址划分、命中判断、替换、容量计算题其实都会变成机械计算。

posted @ 2026-09-03 16:34  陆舟LandBoat  阅读(20)  评论(0)    收藏  举报