14 KiB
tags, create time
| tags | create time | |||
|---|---|---|---|---|
|
2026-06-15 10:00 |
存储系统与 Cache
概述
本文档系统讲解存储层次结构、Cache 的工作原理(映像方式、替换策略、写策略)、命中率计算以及 17 种 Cache 优化技术。Cache 是计算机系统结构的核心考点,几乎每年都有大分值的计算和分析题。
[!tip] 考试重点 Cache 命中率计算、平均访存时间计算、分离 Cache vs 混合 Cache 的性能对比是必考内容。17 种优化技术的分类和代表性技术需熟记。
正文
一、存储层次结构
graph TD
A["Register"] --> B["L1 Cache"]
B --> C["L2 Cache"]
C --> D["Main Memory"]
D --> E["Disk/SSD"]
style A fill:#ff8a80
style B fill:#ff80ab
style C fill:#ea80fc
style D fill:#82b1ff
style E fill:#80d8ff
| 层次 | 容量 | 速度 | 价格/位 | 管理方式 |
|---|---|---|---|---|
| 寄存器 | KB | 最快 | 最贵 | 编译器 |
| L1 Cache | 32~64 KB | ~1 ns | 较贵 | 硬件 |
| L2 Cache | 256 KB~1 MB | ~5 ns | 中等 | 硬件 |
| 主存 | 4~64 GB | ~100 ns | 便宜 | OS+硬件 |
| 磁盘 | TB 级 | ~10 ms | 最便宜 | OS |
[!question] 为什么存储系统要分层? 单一存储器无法同时满足"大容量、高速度、低价格"的要求。分层设计利用局部性原理,将频繁访问的数据放在快速但小的上层,不常用的放在慢速但大的下层,以较低成本获得接近最快速度的平均性能。
二、Cache 基本原理
2.1 映像方式
| 映像方式 | 原理 | 优点 | 缺点 |
|---|---|---|---|
| 直接映像 | 主存块只能映射到 Cache 的固定位置 | 简单、命中时间短 | 冲突不命中率高 |
| 全相联 | 主存块可映射到 Cache 任意位置 | 冲突不命中率最低 | 硬件开销大、查找慢 |
| 组相联 | 折中方案,分为 2^n 组,组内全相联 |
平衡性能和开销 | 复杂度适中 |
[!note] 组相联 Cache 的命名 "
n路组相联"表示每组有n个 Cache 行。2 路组相联 = 每组 2 行,4 路组相联 = 每组 4 行。
地址解析格式:
处理器发出的地址需要被拆分为 Tag / Index / Offset 三部分,不同映像方式的拆分规则不同:
graph LR
subgraph "直接映像地址"
A1["Tag"] --- A2["Index"] --- A3["Block Offset"]
end
subgraph "全相联地址"
B1["Tag"] --- B2["Block Offset"]
end
subgraph "组相联地址"
C1["Tag"] --- C2["Set Index"] --- C3["Block Offset"]
end
各字段含义:
| 字段 | 含义 | 位数计算 |
|---|---|---|
| Block Offset | 块内偏移,定位块中具体哪个字节 | \log_2(\text{块大小}) |
| Index(直接映像) | 定位 Cache 中的哪一行 | \log_2(\text{Cache 行数}) |
| Set Index(组相联) | 定位 Cache 中的哪一组 | \log_2(\text{组数}) |
| Tag | 标记位,用于比对确认是否命中 | 地址总位数 - Index位数 - Offset位数 |
[!example] 地址位分解示例
已知:32 位地址,Cache 容量 16KB,块大小 64B,采用 4 路组相联。
计算各字段位数:
- Offset 位数 =
\log_2(64) = 6位- Cache 行数 =
16\text{KB} / 64\text{B} = 256行- 组数 =
256 / 4 = 64组- Set Index 位数 =
\log_2(64) = 6位- Tag 位数 =
32 - 6 - 6 = 20位地址
0x1A2B3C47的解析:
字段 位数 二进制值 Tag(高 20 位) 31~12 0001 1010 0010 1011 0011Set Index(6 位) 11~6 110001= 第 49 组Offset(6 位) 5~0 000111= 块内第 7 字节[!question] 如果改成直接映像呢? 直接映像:Index =
\log_2(256) = 8位,Tag =32 - 8 - 6 = 18位。 同一地址会映射到第10010001= 第 145 行,而不是第 49 组的某一行。 相联度越高,Index 位数越少,Tag 位数越多,硬件比对逻辑越复杂。
2.2 替换策略
| 策略 | 原理 | 特点 |
|---|---|---|
| LRU(最近最少使用) | 替换最久未被访问的块 | 最常用,效果好 |
| FIFO | 替换最早进入的块 | 简单,但可能替换活跃块 |
| 随机 | 随机选择替换块 | 实现简单,效果尚可 |
2.3 写策略
| 策略 | 命中时 | 不命中时 | 一致性 |
|---|---|---|---|
| 写直达 | 同时写 Cache 和主存 | Write-Allocate 或 No-Write-Allocate | 简单但总线流量大 |
| 写回 | 只写 Cache,标记为脏 | Write-Allocate | 复杂但总线流量小 |
三、Cache 性能计算
3.1 命中率与不命中率
\text{不命中率} = w_I \times mr_I + w_D \times mr_D
其中 $w_I$、w_D 分别为指令和数据的访问占比,$mr_I$、mr_D 为各自的不命中率。
[!example] 完整例题:地址序列命中/不命中判断
已知:直接映像 Cache,4 行,块大小 16B(4 个字,每字 4B)。地址按字节编址。
地址结构:
[ Tag(高位) | Index(2位) | Offset(4位) ]
- Index 位数 =
\log_2(4) = 2位(4 行直接映像)- Offset 位数 =
\log_2(16) = 4位(块大小 16B)访问以下地址序列(十进制,假设地址按字节编址):
序号 地址(十进制) 地址(二进制) Tag Index Offset 块号 行号 结果 1 0 000000000 00 0000 0 0 不命中(冷启动) 2 4 000001000 00 0100 0 0 命中(同一块) 3 16 000100000 01 0000 1 1 不命中(新行) 4 132 100001008 01 0100 8 1 不命中(替换行 1) 5 136 100010008 01 1000 8 1 命中(同一块) 6 64 010000004 00 0000 4 0 不命中(替换行 0) 7 48 001100003 00 0000 3 0 不命中(替换行 0) 8 64 010000004 00 0000 4 0 不命中(已被替换) Cache 行状态变化:
步骤 行 0 行 1 行 2 行 3 初始 空 空 空 空 访问 0 Tag=0 空 空 空 访问 4 Tag=0 空 空 空 访问 16 Tag=0 Tag=0 空 空 访问 132 Tag=0 Tag=8 空 空 访问 64 Tag=4 Tag=8 空 空 访问 48 Tag=3 Tag=8 空 空 访问 64 Tag=4 Tag=8 空 空 命中率 =
2/8 = 25\%[!question] 观察到了什么? 地址 64 在第 6 次访问时被调入,但第 7 次访问 48 时将它替换出,导致第 8 次再次访问 64 时又不命中——这就是典型的**颠簸(Thrashing)**现象。解决方法:提高相联度或增大 Cache 容量。
3.2 平均访存时间
\text{平均访存时间} = \text{命中时间} + \text{不命中率} \times \text{不命中开销}
[!example] 分离 Cache vs 混合 Cache(详解)
已知条件:
- 程序中 78% 是指令访问(取指),22% 是数据访问(load/store)
- 不命中开销均为 40 周期
方案一:分离 Cache(指令 16KB + 数据 16KB = 共 32KB)
- 指令 Cache 不命中率 $mr_I = 1%$,数据 Cache 不命中率
mr_D = 5\%- 命中时间均为 1 周期(指令和数据各有独立端口,不会冲突)
逐步计算:
- 指令部分平均访存时间 =
1 + 0.01 \times 40 = 1.40周期- 数据部分平均访存时间 =
1 + 0.05 \times 40 = 3.00周期- 加权平均 =
0.78 \times 1.40 + 0.22 \times 3.00 = 1.092 + 0.660 = \mathbf{1.752}周期方案二:混合 Cache(统一 32KB)
- 整体不命中率 $mr = 1.5%$(因为容量相同,但指令和数据共享,冲突不命中增加)
- 命中时间:取指仍为 1 周期,但 load/store 需 +1 周期(因为指令和数据共用端口,load/store 需要额外仲裁)
逐步计算:
- 取指平均访存时间 =
1 + 0.015 \times 40 = 1.60周期- 数据平均访存时间 =
2 + 0.015 \times 40 = 2.60周期(命中时间 2 周期)- 加权平均 =
0.78 \times 1.60 + 0.22 \times 2.60 = 1.248 + 0.572 = \mathbf{1.820}周期结论:分离 Cache 更优(1.752 < 1.820),节省约
3.7\%的平均访存时间。[!question] 什么时候混合 Cache 反而更好? 当数据和指令的访问模式不均匀时——比如某个阶段全是取指(循环),另一阶段全是数据访问——混合 Cache 能让 32KB 容量被充分利用,而分离 Cache 各 16KB 可能不够用。此时混合 Cache 的不命中率可能显著低于分离方案。
3.3 CPU 时间与 Cache 的关系
CPU 时间 = IC \times (CPI_{exe} + \frac{\text{访存次数}}{指令} \times mr) \times \tau
四、17 种 Cache 优化技术
17 种优化技术围绕 Cache 性能公式中的三个因素展开,分类关系如下:
graph TD
ROOT["Cache 性能优化"] --> A["降低不命中率 (8种)"]
ROOT --> B["减少不命中开销 (5种)"]
ROOT --> C["减少命中时间 (4种)"]
A --> A1["增大块大小"]
A --> A2["增大 Cache 容量"]
A --> A3["提高相联度"]
A --> A4["伪相联 Cache"]
A --> A5["硬件预取"]
A --> A6["编译器控制预取"]
A --> A7["编译优化"]
A --> A8["Victim Cache"]
B --> B1["非阻塞 Cache"]
B --> B2["写合并"]
B --> B3["请求字优先"]
B --> B4["写缓冲"]
B --> B5["早重启"]
C --> C1["小容量简单 Cache"]
C --> C2["虚拟 Cache"]
C --> C3["流水化 Cache 访问"]
C --> C4["路预测"]
| 分类 | 技术 | 基本思想 | 影响 |
|---|---|---|---|
| 降低不命中率(8种) | 增大块大小 | 利用空间局部性 | 块过大会增加不命中开销 |
| 增大 Cache 容量 | 利用时间局部性 | 增加命中时间 | |
| 提高相联度 | 减少冲突不命中 | 增加命中时间 | |
| 伪相联 Cache | 降低冲突不命中 | 复杂度适中 | |
| 硬件预取 | 提前调入数据 | 增加总线流量 | |
| 编译器控制预取 | 编译器插入预取指令 | 增加指令开销 | |
| 编译优化 | 优化访问模式 | 依赖编译器能力 | |
| Victim Cache | 小全相联 Cache 保存替换出的块 | 减少冲突不命中 | |
| 减少不命中开销(5种) | 非阻塞 Cache | 不命中时允许后续请求 | 提高流水线效率 |
| 写合并 | 合并多次写操作 | 减少写缓冲区占用 | |
| 请求字优先 | 优先传输请求的字 | 减少等待时间 | |
| 写缓冲 | 写操作进入缓冲区异步执行 | 减少写延迟 | |
| 早重启 | 不命中时尽早返回请求字 | 减少等待时间 | |
| 减少命中时间(4种) | 小容量简单 Cache | 降低命中时间 | 适用于 L1 Cache |
| 虚拟 Cache | 用虚拟地址直接索引 | 增加别名问题 | |
| 流水化 Cache 访问 | Cache 访问分段流水化 | 提高时钟频率 | |
| 路预测 | 预测 Cache 行所在路 | 预测错误时需重取 |
[!warning] 优化技术的权衡 几乎每种优化技术都存在"收益-代价"权衡。例如增大块大小降低了强制性不命中,但增加了不命中开销;提高相联度降低了冲突不命中,但增加了命中时间。设计时需要根据具体场景权衡。
五、不命中的三种类型
| 类型 | 原因 | 解决方法 |
|---|---|---|
| 强制性不命中(Compulsory) | 首次访问某块 | 增大块大小、预取 |
| 容量不命中(Capacity) | Cache 容量不足 | 增大 Cache 容量 |
| 冲突不命中(Conflict) | 映射冲突 | 提高相联度、Victim Cache |
六、综合计算题
[!example] 综合计算题:Cache 参数计算与性能分析
已知条件:
- 处理器 32 位地址
- Cache 容量 32KB
- 块大小 64B
- 采用 8 路组相联
- LRU 替换策略
- 写回策略
- 命中时间 1 个时钟周期
- 不命中开销 100 个时钟周期
- 不命中率 2%
- 程序中 30% 为访存指令(load/store)
问题 1:计算 Tag / Index / Offset 位数
- Cache 行数 =
32\text{KB} / 64\text{B} = 512行- 组数 =
512 / 8 = 64组- Offset 位数 =
\log_2(64) = 6位- Index 位数 =
\log_2(64) = 6位- Tag 位数 =
32 - 6 - 6 = 20位
字段 Tag Set Index Block Offset 位数 20 6 6 位置 [31:12] [11:6] [5:0] 问题 2:计算平均访存时间
\text{AMAT} = \text{命中时间} + \text{不命中率} \times \text{不命中开销}= 1 + 0.02 \times 100 = 1 + 2 = \mathbf{3 \text{ 周期}}问题 3:计算对 CPI 的影响
假设理想 CPI(无 Cache 不命中)为 2.0:
\text{实际 CPI} = \text{CPI}_{exe} + \text{访存指令比例} \times \text{不命中率} \times \text{不命中开销}= 2.0 + 0.30 \times 0.02 \times 100 = 2.0 + 0.6 = \mathbf{2.6}Cache 不命中使 CPI 增加了 $0.6$,性能下降了 $30%$。
问题 4:如果将 Cache 容量增大到 64KB(不命中率降为 1%),是否值得?
新的 CPI =
2.0 + 0.30 \times 0.01 \times 100 = 2.0 + 0.3 = 2.3性能提升 =
(2.6 - 2.3) / 2.6 = 11.5\%[!question] 如何权衡? 64KB Cache 的面积是 32KB 的约 2 倍,但性能仅提升 11.5%。若芯片面积紧张,可考虑用其他优化技术(如提高相联度、硬件预取)来替代简单增大容量——这就是17 种优化技术的组合运用。