4. Cache 替换算法:LRU、LFU、随机替换、伪LRU 算法原理与对比

当 Cache 发生缺失(Cache Miss)且当前 Cache 行已被占满时,必须从主存调入新数据块,并选择一个旧块替换出去。这个“选择牺牲块”的策略就是 Cache 替换算法。替换算法的优劣直接影响 Cache 的命中率、硬件开销和功耗。本节深入剖析四种经典替换算法的原理、实现方式与适用场景。

核心权衡: 替换算法需要在“命中率最优”与“硬件可实现性”之间做取舍。越复杂的算法通常命中率越高,但延迟和面积开销也越大。

4.1 替换算法全景对比

下表从核心思想、硬件开销、性能特征和典型应用四个维度,对四种算法进行宏观对比:

算法 核心思想 硬件开销 时间局部性适配 抗扫描干扰 典型应用
LRU 替换最久未访问的块 高(需记录访问顺序) 优秀 弱 CPU L1/L2 Cache
LFU 替换访问次数最少的块 高(需维护计数器) 一般 强 数据库缓冲池、Web Cache
随机替换 随机选择一个牺牲块 极低(伪随机数) 差 强 TLB、大容量 LLC
伪LRU (Tree-PLRU) 用二叉树近似LRU 低(log₂(N) 位) 良好 中等 现代高性能 CPU Cache

4.2 LRU(最近最少使用)算法

LRU(Least Recently Used) 基于时间局部性原理:最近被访问过的数据,在不久的将来很可能再次被访问。因此,它选择替换 最长时间未被访问 的 Cache 行。

4.2.1 实现方式

  • 全相联 LRU(硬件栈法): 使用一个硬件栈维护访问顺序。每次命中时,将命中块移到栈顶;缺失时,替换栈底块。
  • 组相联 LRU(计数器法): 每组内为每行维护一个计数器。访问某行时,该行计数器置 0,其余行计数器 +1;替换时选择计数器值最大的行。
组相联 LRU 计数器更新规则(以 4 路组相联为例):
// 假设组内有 4 行,计数器 age[0..3]
// 初始所有 age = 0

// 情况1:命中行 i
age[i] = 0;                     // 重置为最新
for (j = 0; j < 4; j++) {
    if (j != i) age[j]++;       // 其他行老化
}

// 情况2:缺失且组已满
victim = argmax(age[0..3]);     // 选择 age 最大的行
替换 victim 行,并设置 age[victim] = 0;

4.2.2 LRU 的优缺点

  • 优点: 对时间局部性强的程序命中率极高;行为可预测。
  • 缺点: 硬件实现复杂(N 路需要 O(N) 比较器或移位寄存器);对“扫描”型访问模式(一次性遍历大量数据)表现极差,会污染 Cache。
LRU 替换过程(4路组相联) 初始访问顺序: 块 A(最新) 块 B 块 C 块 D(最旧) 访问块 B 访问后顺序: 块 B(最新) 块 A 块 C 块 D(最旧) 缺失:替换块 D 块 E(新)

4.3 LFU(最不经常使用)算法

LFU(Least Frequently Used) 基于频率局部性:访问次数越多的数据,未来被访问的可能性越大。它选择替换 访问计数最小 的 Cache 行。

4.3.1 实现机制

  • 每行维护一个计数器,每次命中时计数器 +1。
  • 替换时选择计数器值最小的行;若多个行计数相同,通常使用 LRU 或 FIFO 作为辅助策略(称为 LFU-Aging)。
  • 关键问题: 计数器可能无限增长,需要定期老化或衰减,否则旧数据会永久占据 Cache。
LFU 的“缓存污染”陷阱: 如果一个块在早期被频繁访问(如启动阶段),其计数器会变得很大,即使后期不再使用,也很难被替换出去。这导致 Cache 被“冷数据”长期占据。解决方案包括:引入衰减因子、使用滑动窗口计数、或结合 LRU 做混合策略。

4.3.2 LFU vs LRU 对比

对比维度 LRU LFU
核心度量 最近访问时间 历史访问频率
对突发访问 响应快,立即提升优先级 响应慢,需要多次累积计数
对周期性访问 可能被中间访问干扰 稳定保留高频数据
硬件计数器宽度 log₂(路数) 位 通常 4~8 位(需防溢出)
适用场景 CPU Cache(时间局部性主导) 数据库、CDN(频率分布倾斜)

4.4 随机替换算法

随机替换(Random Replacement) 是最简单的策略:当需要替换时,从组内所有有效行中 随机 选择一个作为牺牲块。

4.4.1 实现与特点

  • 实现: 只需一个伪随机数生成器(LFSR 或 Tausworthe),硬件开销极低。
  • 优点: 无状态、无计数器、无比较器;抗扫描干扰能力强(不会像 LRU 那样被全遍历污染)。
  • 缺点: 命中率通常低于 LRU,尤其在时间局部性强的程序中;行为不可预测,导致实时系统难以分析最坏情况。
随机替换的“意外优势”: 在多核共享 LLC(Last-Level Cache)中,随机替换可以避免“核间干扰”导致的 LRU 抖动问题。因此,许多现代处理器(如 Intel 的某些 LLC 切片)在特定条件下使用随机或近似随机替换。

4.5 伪LRU(Tree-PLRU)算法

伪LRU(Pseudo-LRU,通常实现为 Tree-PLRU) 是 LRU 的高效近似。它使用一棵二叉树来记录访问顺序,仅需 N-1 位(N 为路数)即可实现接近 LRU 的命中率。

4.5.1 二叉树原理

  • 对于 N 路组相联 Cache,构建一棵深度为 log₂(N) 的完全二叉树。
  • 每个内部节点有一个方向位(0 或 1),表示“最近访问的块在左子树还是右子树”。
  • 每次访问一个块时,从根节点到该块叶子节点的路径上,所有方向位都指向该块所在子树。
  • 替换时,从根节点开始,沿着方向位指示的“非最近访问”路径下行,直到叶子节点,该叶子节点即为牺牲块。
4路 Tree-PLRU 二叉树结构 根节点 方向位: 0 左子节点 方向位: 1 右子节点 方向位: 0 行 0 行 1 行 2 行 3 替换路径:根(0) → 左子(1) → 行1(牺牲块)

4.5.2 伪LRU 的优缺点

  • 优点: 硬件开销极低(仅 N-1 位寄存器 + 简单组合逻辑);命中率非常接近理想 LRU(通常差距 < 2%)。
  • 缺点: 并非严格 LRU,在某些特定访问模式下可能做出次优选择;对路数较多的 Cache(如 16 路以上)性能略有下降。
工程实践: 现代高性能 CPU(如 ARM Cortex-A78、Intel Golden Cove)的 L2/L3 Cache 普遍采用 Tree-PLRU 或其变体。它在“接近 LRU 的命中率”与“极低的硬件成本”之间取得了最佳平衡。

4.6 算法综合对比与选型指南

下表从硬件实现角度给出更详细的对比:

指标 LRU LFU 随机替换 伪LRU (Tree-PLRU)
每路存储开销 log₂(N) 位计数器 4~8 位计数器 0 位 1 位(每内部节点)
总存储位(4路) 4 × 2 = 8 位 4 × 4 = 16 位 0 位 3 位
总存储位(8路) 8 × 3 = 24 位 8 × 4 = 32 位 0 位 7 位
替换延迟 高(需比较器树) 高(需比较器树) 极低(1周期) 低(log₂(N) 级门延迟)
命中率(典型) 基准(100%) 90%~95% 80%~90% 98%~99.5%
抗扫描能力 弱 强 强 中等

4.6.1 选型建议

  • CPU L1 Cache(4~8路): 优先使用 Tree-PLRU,兼顾性能与面积。
  • CPU L2/L3 Cache(8~16路): 使用 Tree-PLRU 或 改进型 LRU(如 RRIP,Re-Reference Interval Prediction)。
  • TLB(全相联或组相联): 常用 随机替换 或 伪LRU,因为 TLB 缺失代价高但路数少。
  • 数据库/Web Cache(大容量、频率分布倾斜): 使用 LFU-Aging 或 ARC(Adaptive Replacement Cache)。
  • 实时/安全系统: 避免使用随机替换,优先选择 LRU 或 伪LRU 以保证行为可预测。
关键结论: 没有一种替换算法在所有场景下都是最优的。实际工程中,常根据工作负载特征动态切换算法(如 Intel 的 DIP(Dynamic Insertion Policy)),或在硬件中提供可编程替换策略寄存器。