B — 缓存层级

CPU 缓存是位于 CPU 与主存之间的高速小容量存储器,大小从 32KB 到数十 MB,用来弥合 CPU 速度与主存速度之间的鸿沟。

缓存层次结构

graph TD
 A["CPU 寄存器<br/>~0.3ns, ~100B"] --> B["L1 缓存 (指令 + 数据)<br/>~1ns, 32KB"]
 B --> C["L2 缓存<br/>~5ns, 256KB"]
 C --> D["L3 缓存 (共享)<br/>~20ns, 8MB"]
 D --> E["主存 (DRAM)<br/>~100ns, 16GB"]
 E --> F["磁盘 / SSD<br/>>10us, TB级"]
存储层级延迟(约)容量带宽
L1 Cache~1 ns32KB/核~1 TB/s
L2 Cache~5 ns256KB/核~500 GB/s
L3 Cache~20 ns8MB/共享~200 GB/s
DRAM~100 ns16GB~50 GB/s
NVMe SSD~10 usTB~5 GB/s

Cache Line

CPU 不以字节、也不以机器字为单位从主存读取数据,而是以 Cache Line 为单位,一个 cache line 通常为 64 字节(x86 和 ARM)。

内存视角: [Byte 0] [Byte 1] ... [Byte 63] [Byte 64] ... [Byte 127] ...
Cache Line: [ Line 0 ] [ Line 1 ]

每次 CPU 读取任何一个地址,都会将包含该地址的整个 64 字节 cache line 从主存拖入缓存。

Cache Miss 与空间局部性

场景行为延迟
Cache Hit数据已在缓存中~1 ns
Cache Miss需从主存/下级缓存加载~100 ns

连续存储的数据结构(数组、vector)利用空间局部性:访问 arr[i] 时,arr[i+1]arr[i+15] 已被预取到同一条 cache line 中。节点存储的数据结构(链表、树)不具备这个性质。

vector 遍历: arr[0](hit), arr[1](hit), arr[2](hit), ... arr[15](hit), arr[16](miss→hit)
list 遍历: node[0](miss→hit), node[1](miss→hit), node[2](miss→hit), ...
 ↑ 每个节点地址散列,几乎每次都是 miss

缓存关联度(Associativity)

缓存被划分为多个组(Set),每个组包含若干条缓存行(Way):

类型特点
直接映射(1-way)每个内存地址只能映射到唯一的 cache line,冲突多
全关联任何地址可放入任何 cache line,电路复杂
N 路组关联折中方案:组间直接映射,组内 N 路全关联(主流)

现代桌面 CPU 的 L1 缓存通常是 8 路组关联,L3 为 16 路或以上。

伪共享(False Sharing)

两个不同线程分别修改两个不同的变量,但它们恰好在同一条 cache line 中,会导致缓存一致性协议(如 MESI)在两个 CPU 核之间频繁传输这条 cache line,产生性能灾难。

// 两个线程各递增自己的计数器,但计数器在同一条 cache line 中
struct Counters {
 int counter_a; // 线程 A 递增
 int counter_b; // 线程 B 递增
 // 两个 int 共 8 字节,在同一条 64 字节 cache line 中
 // → 每次递增都触发 cache line 在核间 bounce
};
 
// 修复:加 padding 填满一条 cache line
struct PaddedCounters {
 int counter_a;
 char _pad[60]; // 填充到 64 字节边界
 int counter_b;
};

对容器的意义:多线程下对 vector<int> 的不同位置并发写入,如果两个线程分别写相邻元素,同样会触发伪共享。解决方式与上面相同——保证每个线程写入的元素间隔至少 64 字节。

写策略

策略行为适用场景
Write-Through写缓存时同步写入主存,一致性简单但慢部分嵌入式系统
Write-Back只写缓存,标记 dirty,evict 时才写回主存主流 x86/ARM,性能高

缓存性能优化策略实践

以下是常见的缓存友好编程模式:

策略说明示例
顺序访问按连续地址遍历数据for(i=0;i<n;i++) a[i]
避免 stride 访问不要以大于 cache line 的步长跳跃访问避免 for(i=0;i<n;i+=16) a[i]
结构体 vs 数组对空间局部性要求高的访问用 Array of Structs;对计算密集型用 Struct of ArraysSoA 适合向量化
循环分块(Tiling)将大循环拆分成适合缓存的小块矩阵乘法分块优化

对容器的意义

  • std::vector 天然保证顺序访问,缓存友好
  • std::deque 在同一 block 内缓存友好,跨 block 时依赖 block 的分配位置
  • std::list 几乎不利用缓存,每次解引用大概率 cache miss

本章与其他模块的链接