1. 缓存里面到底存了什么?
以32KB L1 Data Cache,8 路组相联,64 字节 Cache Line为例。
可以把整个缓存看成一张二维表:
- 共有64 组(Set 0 ~ Set 63)
- 每组有8 路(Way 0 ~ Way 7)
- 每路就是一个缓存条目,里面包含:
组成部分 | 说明 |
Valid bit | 1 位,表示这个条目里是否存放了有效数据(上电或刷新后为 0,防止误匹配) |
Dirty bit | 1 位(写回策略时用),表示这行数据被修改过,和内存不一致,将来替换时要写回 |
Tag | 地址的高位部分,用来唯一标识这个 Cache Line 对应的是哪一块内存 |
Data Block | 64 字节的连续数据,也就是常说的那个Cache Line 数据 |
(可能还有)LRU 位 | 用于记录访问历史,帮助替换策略选择丢掉哪一路 |
所以,“缓存行”不只是一个数据块,它是Tag + 状态位 + 64 字节数据块的整体。
2. 怎么判断数据在不在缓存里?地址被怎样拆分?
假设我们运行在32 位物理地址的 CPU 上(举例方便)。
缓存配置:32KB,8 路组相联,行大小 64 字节。
2.1 先算出三个部分的位数
- 块内偏移 Offset:64 = 2⁶,需要6 位,用于在 64 字节里选具体字节。
- 组数:总条目 = 32KB / 64B = 512。8 路组相联 ⇒ 组数 = 512 / 8 = 64 组。需要6 位组索引。
- Tag 位:剩下的高位 = 32 - 6 - 6 =20 位。
2.2 地址拆分图(32 位物理地址)
|←——— Tag 20 位 ———→|← Index 6 位 →|← Offset 6 位 →| [31 .. 12] [11 .. 6] [5 .. 0]字段 | 位数 | 位域(示例) | 作用 |
Tag | 20 | [31:12] | 存放在缓存条目里,用来“对号”确认是哪一块内存 |
Index | 6 | [11:6] | 选出64 组中的哪一组 |
Offset | 6 | [5:0] | 在 64 字节的块里,找到CPU 真正要的那个字节 |
3. 查找过程:Tag、Index、Offset 如何联动
当 CPU 执行一条 LOAD 指令,比如mov eax, [0x12345678],物理地址就是0x12345678。
- 拆地址
- Offset = 低 6 位
- Index = (地址 >> 6) 的低 6 位,比如得到 0x1A(第 26 组)
- Tag = 剩下的高位 0x48D15 之类
- 选中组
用 Index 找到第 26 组,这个组里有 8 个 Way。 - 并行比较 Tag
把该组 8 路的Tag 和 Valid 位一起读出来(实际硬件同时做),和当前地址的 Tag 比对,而且要求 Valid=1。 - 命中 (Hit)
假如 Way 3 的 Tag 完全相等,且 V=1,那就是命中。
- 选中 Way 3 的 64 字节 Data Block
- 用 Offset 从这 64 字节中提取所需的 4 字节(或 1 字节),返回给 CPU 寄存器
- 更新 LRU 信息,表示 Way 3 刚被访问过
- 缺失 (Miss)
如果 8 路里没有任何一个 Tag 匹配,或者 V=0,就是未命中。
- 硬件暂停流水线,向下一级存储(L2 / L3 / 内存)发出“读取整块 64 字节”的请求
- 数据回来后,放入当前组(第 26 组)的某一 Way,比如根据 LRU 选一个最近最少使用的 Way
- 如果要替换的 Way 的 Dirty bit 为 1,说明它曾修改过,需要先把这 64 字节写回内存
- 将新块的 Tag 写入该 Way,设置 Valid=1,Dirty=0
- 然后像命中一样,从新块里用 Offset 取数据返回 CPU
4. 其他关键标志位:Valid & Dirty 等
- Valid (V):开机时所有 Valid=0。不加这一位,垃圾 Tag 可能和数据匹配,读到错数据。
- Dirty (D):只在写回(write-back)策略下有效。如果 CPU 写过这个缓存行,D=1,表示它比内存新。将来被替换时,必须写回内存;如果 D=0,可以直接丢弃。
- LRU 位 / 伪 LRU 位:记录这一组里各 Way 的访问新旧顺序,用来决定踢谁。
- MESI 等一致性状态位:多核系统里,标记该行是 Modified / Exclusive / Shared / Invalid,保证多核看到的数据一致。
5. 完整流程:从一条 LOAD 指令到拿到数据
以物理地址访问 L1 Data Cache为例,屏蔽 TLB 细节。
- 指令发出
LOAD R1, [A](A 是物理地址) - 地址拆分
硬件抽取 Index(6 位)、Offset(6 位)、Tag(高位) - 访问 L1 数据缓存
- 用 Index 找到 Set
- 读出该 Set 所有 8 路的 Tag、Valid、Dirty、Data 块
- Tag 比较 + Valid 检查
- 比较器组并行工作:
(Way[i].Tag == A.Tag) && Way[i].Valid
- 比较器组并行工作:
- 命中路径
- 选路信号控制一个多路选择器,挑中 Way[i] 的 64 字节数据
- 再根据 Offset 做字节移位/选择,得到最终数据
- 数据返回 CPU,LOAD 完成
- 缺失路径
- 生成缺失请求(物理地址 A),发往 L2
- L2 同样用它的 Index/Tag 查找,若 L2 命中则返回整块 64B;否则继续向 L3/内存
- 最终 64 字节数据抵达 L1,填入指定 Set 的替换 Way
- 更新 Tag、Valid、重置 Dirty,可能更新 LRU
- 然后重新执行第 3 步(或直接旁路将数据同时返回 CPU)
6. 一个刻在脑子里的生活比喻:图书馆档案室
想象一个图书馆档案室:
- 馆内有64 个书架→ 这对应64 组 (Set)
- 每个书架有8 层→ 这对应8 路 (Way)
- 每层可以放一个档案盒→ 这就是一个Cache Line 条目
- 档案盒里正好有64 页纸→ 这对应64 字节数据块
- 档案盒书脊上贴着标签 (Tag)→ 写明了“这是哪一本档案的第几卷”
- 盒子上还有一张“有效”便利贴 (Valid)→ 空盒子没有便利贴,不能拿给读者
- 如果有人在盒子里涂改了,就贴一张“已修改”(Dirty)便利贴
现在,有读者要查阅地址 A的第 K 页:
- 根据地址里的书架编号 (Index),直接走到那个书架前。
- 扫一眼这个书架的8 层,比较书脊上的标签(Tag)和地址里的 Tag,并且必须看到“有效”便利贴。
- 如果找到标签匹配、且有效的档案盒 →命中。
从盒子里翻到第 K 页 (Offset),交给读者。 - 如果这个书架 8 层都没有匹配的标签(或者盒子无效)→缺失。
管理员去仓库(内存)找到对应的档案盒,拿回来:
- 如果书架满了,根据“最少借阅”记录选一层,把旧盒子取下来。
- 如果旧盒子贴着“已修改”,必须先把它的内容抄回仓库(写回)。
- 新盒子放上去,贴上正确的标签和“有效”便利贴,把第 K 页交给读者。
这样一来:
- 缓存行就是那个连盒带签的档案盒。
- 组相联就是书架固定、但书可以灵活放在任意一层。
- Tag/Index/Offset就是找书架、看标签、翻页数的过程。
- Valid/Dirty就是盒子上的便利贴。
有了这个书架模型,整个缓存查找和替换的直觉就非常牢固了,以后很难再混淆。