缓存淘汰 · 从 Bélády 上界到 SIEVE
缓存的容量总是小于数据量, 于是每次 miss 都要回答一个问题: 踢掉谁。这个决定要预测未来 —— 踢掉的应当是最久才会被再次用到的那一个。未来不可知, 于是所有在线策略都在拿过去的两个信号做外推: recency (这个 key 多久没被用过) 与 frequency (它一共被用过几次)。两个信号各有各的失效场景, 一次全表扫描能把 LRU 的命中率打到 0, 一批过气的历史高频 key 能把 LFU 的缓存占满。
本系列的口径是统一的: 同一条访问序列、同一容量下比命中率, 并且总把 Bélády 的离线最优画在同一张图上作参照。实测的四种 workload 里, 最好的在线策略只拿到最优值的 88.7% (Zipf) 到 99.0% (纯循环扫描)。
路线是: 先建立评测口径与上界, 再看两条改良 LRU 的经典路线 (CLOCK 的 reference bit、ARC 的 ghost 列表), 然后是准入判定这条新思路 (W-TinyLFU 的频率草图), 接着是 2023–2024 年那两个反直觉的结果 —— 只用 FIFO 队列的 S3-FIFO 与 SIEVE 在实测里常常赢过 LRU 一族, 最后是 Redis 的工程取舍与容量单调性。
口径: 上界与两个信号
Bélády 1966 给出的 MIN 是同容量下任何 demand paging 策略的命中率上界, 它需要完整的未来访问序列, 因而只能离线跑 —— 但正因如此它是唯一可靠的参照线。有了它, 「某策略命中率 68%」这句话才有意义: 68% 是最优值的多少。
「抗扫描」这句话要看是哪种扫描
上界与经典策略 · 延伸阅读
- Bélády · A study of replacement algorithms for a virtual-storage computer (1966) ieeexplore.ieee.org MIN 算法的原始论文: 淘汰下一次使用最晚的那一页最优, 以及它为何只能离线。本系列所有对照图里的参照线都是它。
- Page replacement algorithm — Wikipedia en.wikipedia.org 总览: FIFO、LRU、CLOCK、second-chance、以及 Bélády 异常与 stack algorithm 的定义。
- Zhou, Philbin & Li · The Multi-Queue Replacement Algorithm (2001) usenix.org 按访问频率分多条队列的早期方案, 是 LRU-K、2Q、ARC 这条线的中间一环。
- O'Neil, O'Neil & Weikum · The LRU-K Page Replacement Algorithm (1993) dl.acm.org 第一个把「倒数第 K 次访问的时刻」当作信号的策略, 首次系统论证了单看最近一次访问信息量不足。
改良 LRU 的两条经典路线
LRU 的两个毛病是: 每次命中都要动链表 (锁竞争与写放大), 以及一次扫描就能把它冲垮。CLOCK 用每项一个 reference bit 解决前者, ARC 用两个 ghost 列表记住「刚被淘汰的是谁」, 靠这条反事实信号自适应地在 recency 与 frequency 之间调配容量。
CLOCK:一个 bit 换掉一条链表
LRU 每次命中都要摘挂链表,在多核上就是一次抢锁。CLOCK 把链表换成环形数组加每项一个 reference bit,命中只置位、淘汰才走针,实测写入量降到 LRU 的十分之一而命中率不降反升。
ARC:ghost 列表给出的反事实信号
把缓存切成「只用过一次」与「用过多次」两段,再各配一个只存 key 的 ghost 列表。ghost 命中说明这一段当初不该那么小,于是目标切分点 p 往那边挪一格,recency 与 frequency 的配比由此自己找平衡。
ARC 与自适应 · 延伸阅读
- Megiddo & Modha · ARC: A Self-Tuning, Low Overhead Replacement Cache (FAST 2003) usenix.org ARC 的原始论文: 四个列表、目标 p 的自适应规则, 以及为何 ghost 列表提供的是反事实信号。
- Jiang, Chen & Zhang · CLOCK-Pro (2005) dl.acm.org 把 LIRS 的 reuse distance 思路装回 CLOCK 的环形结构, 用三只手区分冷热页, Linux 与 NetBSD 的页面回收受它影响。
- Adaptive replacement cache — Wikipedia en.wikipedia.org ARC 的简述与专利史 —— ZFS 用它, 而 PostgreSQL 曾用后又因专利风险换成了 clock sweep。
准入: 先问该不该进来
此前所有策略都在回答「该踢谁」, W-TinyLFU 换了个问题: 新来的这个 key 凭什么进来。它用一个 Count-Min sketch 估计全体 key 的访问频率, 新来者必须比将被淘汰者更热才准入; 计数器周期性减半, 老化由此而来。
频率草图与准入 · 延伸阅读
- Einziger, Friedman & Manes · TinyLFU: A Highly Efficient Cache Admission Policy (2017) arxiv.org TinyLFU 与 W-TinyLFU 的论文: 用近似计数结构做准入判定, 以及计数器减半式的老化。
- Caffeine · Efficiency github.com Caffeine 作者在多组真实 trace 上把 W-TinyLFU 与 LRU、ARC、LIRS 逐一对照的结果, 含自适应 window 的后续改进。
- Cormode & Muthukrishnan · An Improved Data Stream Summary: The Count-Min Sketch (2004) dsf.berkeley.edu Count-Min sketch 的原始论文: 为什么它只会高估不会低估, 以及误差与宽度、深度的关系。
FIFO 家族与工程取舍
2023–2024 年的两个结果把方向掰回了简单: S3-FIFO 与 SIEVE 都只用 FIFO 队列, 命中路径几乎不写元数据, 实测命中率却常常高于 LRU。Redis 的取舍在另一个维度上 —— 它宁可用随机采样近似 LRU, 也不肯为每个对象多存两个指针。
S3-FIFO 与 SIEVE:更简单反而更准
两个新结果都放弃 LRU 链表、只用 FIFO 队列:S3-FIFO 用一条小 FIFO 过滤一次性对象,SIEVE 在单条 FIFO 上加一只 hand 与一位 visited。实测命中率与 ARC 相当,写入量低六十倍以上。
Redis:宁可采样也不要全局链表
Redis 的淘汰不维护任何全局顺序结构:LRU 是随机采样若干 key 取最旧,LFU 是每对象一个 8 位对数计数器加按分钟衰减。本页量化这套近似的质量,并说明它为何比精确 LRU 划算。
容量单调性与策略选型
加内存不一定提高命中率。LRU 与 Bélády 有 stack property 因而单调不降,FIFO 有反例,带 bit 的新策略出现反常的频率比 FIFO 高两个数量级。末尾是全部策略的实测总表与选型判据。
选型先看写入量, 再看命中率
FIFO 家族与 Redis · 延伸阅读
- Yang, Zhang, Qiu, Yue & Vinayak · FIFO queues are all you need for cache eviction (SOSP 2023) dl.acm.org S3-FIFO 的原始论文: 一次性对象在真实 trace 里占多数, 用一条小 FIFO 把它们挡在门外即可。
- Zhang, Yang, Yue, Vigfusson & Rashmi · SIEVE is Simpler than LRU (NSDI 2024) usenix.org SIEVE 的原始论文: 一条 FIFO 加一只 hand, 比 LRU 更简单却在 1559 条真实 trace 上更常胜出。
- Redis · Key eviction redis.io maxmemory-policy 的八种取值、近似 LRU 的采样机制与 maxmemory-samples 的取值建议, 以及 LFU 模式的两个参数。
-
redis/src/evict.c
github.com
一手实现:
evictionPoolPopulate的 16 项淘汰池、LFULogIncr的概率增长与LFUDecrAndReturn的衰减。 - SIEVE 项目主页 cachemon.github.io 作者维护的实现清单与 libCacheSim 基准工具, 本系列的定性结论可在其上用真实 trace 复核。