「内存墙」靠存储层次弥合——小块高速存储挡住大部分访问。前提(局部性)+ Cache 三件套(映射/替换/写策略);第 8 章推到虚拟内存。
Cache 有效的前提是程序访问内存不均匀:
for (int i = 0; i < n; i++) // i:时间局部(反复读)
sum += a[i]; // a[i]:空间局部(连续读)
注意:局部性是「程序的统计规律」,非硬件保证。链表遍历、哈希表随机访问是反局部性——
p = p->next每次跳随机堆地址,Cache 命中率暴跌。故「数组 vs 链表」遍历性能差一个数量级。
从快到慢、从小到大:
寄存器 < L1 Cache < L2 < L3 < 主存 < 磁盘
~0.3ns ~1ns ~4ns ~12ns ~100ns ~10ms
衡量指标:
平均访存时间 = 命中时间 + 缺失率 × 缺失代价
缺失代价 = 从下一层取数的延迟,远大于命中时间,故降缺失率是 Cache 第一目标。
内存地址切成 标记 Tag | 索引 Index | 块内偏移 Offset。三种映射决定「内存块能放 Cache 哪个位置」:
| 映射 | 规则 | 查找方式 | 冲突缺失 | 硬件 |
|---|---|---|---|---|
| 直接映射 | 每块固定放唯一位置(Index 决定) | 只查 1 个位置 | 高(同 Index 不同 Tag 互踢) | 最简单 |
| 全相联 | 可放任意位置 | 并行查所有 Tag(内容寻址) | 无冲突缺失 | 最贵(CAM) |
| 组相联 | 先按 Index 定组,组内任意 | 查一组(如 8 路)的 Tag | 介于两者 | 折中,主流 |
直接映射: 块只能进它唯一的那一格
组相联(8路):块进它所属组的 8 格之一
全相联: 块能进任何一格
对比:直接映射便宜但同 Index 不同块互相驱逐;全相联零冲突但需 CAM 并行比较所有 Tag、面积功耗爆炸;组相联(8/16 路)是工程甜点——现代 L1 常用 8 路、L2/L3 用 16 路。
组满时新块要踢掉一个旧块:
| 策略 | 规则 | 特点 |
|---|---|---|
| 随机 | 随机踢一个 | 简单,命中率不稳定 |
| FIFO | 踢最早进来的 | 简单,但可能踢掉热点(Belady 异常) |
| LRU | 踢最久没用的 | 贴合时间局部性,命中率高,但硬件要记录访问顺序 |
注意:LRU 理论最优,但硬件代价随路数指数增长(记录 N! 种顺序)。工程用近似 LRU(树形伪 LRU,每路几 bit 记「最近用过」)以低成本逼近。缓存越大、路数越多,越用近似算法。
读缺失好办,写才是一致性麻烦——写会同时改 Cache 和主存:
| 策略 | 写命中时 | 写缺失时 | 特点 |
|---|---|---|---|
| 写直达(write-through) | 同时写 Cache 和主存 | 写分配 / 写不分配 | 主存永远新,简单,但每次写都访存 |
| 写回(write-back) | 只写 Cache,标 dirty | 写分配为主 | 主存延迟到被替换时才写,省带宽 |
对比:写直达简单、一致性好但带宽浪费;写回省带宽、快但要 dirty 位和替换时写回。多核下写回需 MESI 之类协议同步(第 8 章/OS 并发延伸)。CPU 内部 Cache 普遍写回,MMIO 设备(第 9 章)有时强制写直达。
struct 小字段紧邻利于 Cache 行。伪共享(false sharing):两线程写同一 Cache 行不同变量,互相失效对方行、性能雪崩。