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。
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.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)),或在硬件中提供可编程替换策略寄存器。