Lec 14缓存实现
Lec 14 缓存实现
TL;DR
- 相联度化解冲突:从直接映射到 N 路组相联与全相联,通过在同一组内提供多个候选路,以并行比较器开销显著抑制冲突缺失。
- 协同替换与写策略:采用 LRU 近似算法选择牺牲行,结合写回(Write-Back)与脏位(Dirty Bit)最小化向主存写回的带宽压力。
- 3C 缺失分类模型:将未命中解耦为强制缺失、容量缺失与冲突缺失三类,揭示无万能最优配置、需根据访问模式权衡的架构本质。
缓存微架构进阶与冲突权衡
Lec 13 建立了缓存概念与直接映射的基本组织。本讲深入实现:用相联(associativity)减少冲突、用替换策略决定换出谁、用写策略处理写操作,并系统区分缺失类型。
本讲导览
- 冲突缺失与相联度
- 组相联 / 全相联缓存
- 替换策略(LRU)
- 写策略(写直达 / 写回,脏位)
- 缺失三分类
- 综合对比例题
冲突缺失:直接映射的痛点
直接映射缓存“每个地址只能落在唯一一行”太死板。考虑一段循环:指令在字地址 1024、1025、1026,访问的数据在字地址 37、38、39。
- 1024 行的直接映射缓存:指令映射到行 0/1/2,数据映射到行 37/38/39,互不冲突 → 循环稳定后次次命中。
- 但若数据改放在字地址 2048、2049、2050——它们也映射到行 0/1/2,与指令抢同一行!于是每条指令、每次访存都互相覆盖,次次未命中。
这种因“多个地址争抢同一行”导致的未命中叫冲突缺失(conflict miss)。解决办法:给地址更多可去的位置。
组相联与全相联
- 全相联缓存(fully-associative):任意地址可放任意行——彻底消除冲突缺失。但查找时必须并行比较每一行的 tag,需要大量比较器,代价高。
- N 路组相联缓存(N-way set-associative):折中方案,相当于 N 个直接映射缓存并行工作。每个叫一路(way);地址用 index 选中一组(set),该组在每条路里各有一行,于是同一 index 的地址有 N 个可去的位置。
- 大幅减少冲突缺失,又只需 N 个比较器(而非每行一个)。
- 直接映射 = 1 路;全相联 = 只有一组、路数 = 行数。两者是 N 路的两个极端。
地址切分仍是
tag | index | (block offset) | byte offset,只是 index 现在选的是组而非单行。
替换策略
有了多个可放位置,换出时就要选牺牲谁——这就是替换策略(replacement policy)。
- 理想:换出“未来最久才会用到”的——但我们不知道未来。
- LRU(Least Recently Used,最近最少使用):靠局部性论证——最久没用的,近期最不可能再用。实践中效果好、最常用。
- 2 路 LRU 很简单(记哪一路更近用过即可)。
- N 路 LRU 需要记录所有访问的完整顺序(N! 种排列),约需 个 LRU 位 + 复杂逻辑。故现代处理器常用 LRU 的廉价近似。
- 其他:FIFO(最近最少替换)、Random(对抗最坏访问模式时有用)。LRU 总体最常见。
写策略
写操作(如 sw)有两种与主存同步的策略:
- 写直达(write-through):每次写都同时写回主存,保持缓存与主存一致。简单,但慢、浪费带宽(可能写了又很快被覆盖)。
- 写回(write-back):只在该行被替换出缓存时才写回主存。平时只要还在缓存里就从缓存读写。更快,是常用做法。
- 需给每行加一个脏位(dirty bit, D):自调入后被写过则置 1。只有替换一个脏行时才需要先写回主存;只读过(D=0)的行直接丢弃即可。
写回例:16 行、块大小 4 的直接映射缓存(D 位 + V 位 + 24 位 tag),写
0x09到0x4818。0x4818=0b0100_1000_0001_1000→ tag=0x48、index=0x1、block offset 指向块内第 2 个字、byte offset=0。
- 若 index 1 命中(valid=1 且 tag=0x48):把块内第 2 字改成 0x09,置 dirty=1。
- 若未命中(如该行 tag 是 0x280):要替换该行——先查脏位:脏则把旧行(tag 0x280 对应的地址范围)写回主存,再把新行(tag 0x48 对应整块 0x4810–0x481C)整块载入,置 dirty=0,写入新值。
缺失三分类(3C)
| 类型 | 成因 | 缓解手段 |
|---|---|---|
| 强制缺失(compulsory) | 第一次访问某块,缓存里必然没有(冷启动) | 加大块大小(一次多带些) |
| 冲突缺失(conflict) | 多个地址争抢同一组,相联度不够 | 提高相联度 |
| 容量缺失(capacity) | 工作集大于缓存总容量,装不下 | 加大缓存容量 |
综合对比例题:没有“万能配置”
三个缓存均 8 字、块大小 1、LRU:直接映射(DM)、2 路、全相联(FA)。
- 例 1:反复访问
0x0, 0x10, 0x4, 0x24。DM 中 0x4 与 0x24 映射到同一行、互相覆盖 → 命中率 50%;2 路与 FA → 100%。 - 例 2:循环访问
0x0,0x4,0x8,0xC,0x10,0x14,0x18,0x1C,0x20(9 个,缓存只 8 行)。- DM:仅 0x0 与 0x20 冲突 → 7/9;
- 2 路:0x0、0x20、0x10 争 2 个位置 → 6/9;
- FA:9 个值塞 8 行,每次都踢掉马上要用的 → 0(典型容量缺失)。
- 例 3:访问
0x0,0x4,0x8,0xC,0x20,0x24,0x28,0x2C,0x10,三者命中率分别约 1/9、6/9、0。
结论:没有哪种配置永远最好——相联度、块大小、容量、替换策略都是可调的“杠杆”,最优配置取决于具体应用的访问模式。这正是设计缓存(及 Lab)时要权衡的核心。
我的理解
::: insight
相联度边际效益递减与容量越界雪崩效应
缓存微架构的演进深刻揭示了工程设计的非线性折衷:
- 相联度收益的“2的幂次递减”:从直接映射升级到 2 路组相联,可以消除大部分病态冲突缺失;从 2 路升到 4 路或 8 路,未命中率进一步下降;但超过 8 路后,相联度对命中率的改善微乎其微。然而,硬件代价却在急剧攀升——N 路并行比较器不仅显著消耗芯片面积,更重要的是,多路输出的命中判定逻辑与 Mux 延长了命中时间(Hit Time),直接拖慢处理器的时钟主频。因此现代 L1 缓存极少超过 8 路。
- LRU 全相联的容量越界雪崩(Thrashing Disaster):例 2 中展示了一个经典的反直觉现象:当循环遍历的工作集大小恰好为 ( 为全相联缓存容量)时,严格的 LRU 策略会在每一拍精准驱逐下一拍即将访问的数据块,导致整体命中率从理论最大值断崖式暴跌至 0%(甚至劣于直接映射缓存的 7/9 命中率)。
- 自适应替换与抖动抵御:这种极端退化促使现代高级缓存控制器引入伪随机替换、双峰替换策略(Bimodal Insertion Policy, BIP)或基于命中计数的动态自适应插入机制,在检测到大循环扫描模式时主动绕过 LRU 队列,防止整个缓存被单次流式遍历彻底冲刷污染。 :::