算法与数据结构 / 缓存淘汰 · 从 Bélády 上界到 SIEVE / 容量单调性与策略选型 待审核 7 / 7
stack property · Bélády 异常 · 实测总表

容量单调性与策略选型

「命中率不够就加内存」是一条几乎不会被质疑的经验。它对某些策略成立、对另一些不成立,而不成立的那一批里包含本系列后半部分推荐的几个。

1 · stack property

定义 1.1(stack algorithm) 一个淘汰策略具有 stack property,若对任意访问序列与任意容量 cc,容量 cc 的缓存内容始终是容量 c+1c+1 的缓存内容的子集。

这个包含关系直接推出单调性:容量 cc 命中的每一次访问,容量 c+1c+1 也必然命中,故命中率随容量单调不降。

定理 1.2 LRU 具有 stack property。

证明 对访问次数归纳。容量 cc 的 LRU 缓存恰好是「到目前为止最近访问过的 cc 个不同 key」,这个刻画不依赖任何历史决策,只依赖访问序列本身。最近 cc 个显然是最近 c+1c+1 个的子集。∎

Bélády 的 MIN 同样具有这个性质,证明要复杂一些(见 Mattson 等 1970 的原始论文)。LFU 在计数永久保存的理想形态下也是 stack algorithm,但真实实现里 key 被淘汰后计数归零,这一条就不再成立。

实测 400 条长度 30、key 空间 8 的随机序列,容量从 2 扫到 8:LRU 与 Bélády 出现「容量变大命中率下降」的次数都是 0。

2 · FIFO 的反例

FIFO 没有这个性质,最有名的反例只有 12 次访问:

1 2 3 4 1 2 5 1 2 3 4 5
容量 FIFO 命中 LRU 命中 Bélády 命中
2 0 0 3
3 3 2 5
4 2 4 6
5 7 7 7

容量从 3 加到 4,FIFO 的命中数从 3 掉到 2。这就是 Bélády 异常,1969 年由 Bélády、Nelson 与 Shedler 给出。

原因在于 FIFO 的缓存内容不是访问序列的函数,而依赖历史上每一次淘汰的时机。容量 3 时第 7 次访问 5 淘汰的是 3,此后 12 都还在;容量 4 时同一次访问淘汰的是 1,把马上要用的 key 踢掉了。多出来的那一格改变了整条淘汰链的相位。

图 2-1 · 命中率随容量的曲线,下凹处用红点标出。可切换策略与序列,教科书反例与四种合成 workload 都在选项里。

3 · 异常在各策略上的出现率

FIFO 是这个现象的教科书代表,但它并不是最容易出现异常的策略。实测 1000 条长度 30、key 空间 8 的随机序列,容量从 2 扫到 8,统计有多少条序列上出现过下凹:

策略 出现下凹的序列比例
Bélády 0.0%
LRU 0.0%
FIFO 0.1%
LFU 0.5%
W-TinyLFU 2.3%
Redis 近似 LRU 4.3%
ARC 4.5%
CLOCK 5.6%
S3-FIFO 8.6%
SIEVE 10.4%
Redis LFU 23.4%

这个排序与直觉相反。原先预期 FIFO 会名列前茅,毕竟它是这个反例的招牌;实测它只有 0.1%,比除 LRU 与 Bélády 之外的所有策略都低。原因是均匀随机序列上 FIFO 的相位很难错开到坏处,而 reference bit、visited bit、2 位计数器这些状态一进来,缓存内容就更强地依赖历史路径,多一格容量能改变的东西更多。Redis LFU 那 23.4% 里还叠了采样的随机性。

换成本系列的四种合成 workload、容量从 10 扫到 400(步长 10),下凹仍然存在但幅度很小:突发热点上 SIEVE 有 9 处、最大跌 0.85 个百分点,S3-FIFO 有 2 处、最大 0.51;混合 workload 上 Redis LFU 有 3 处、最大 0.14。FIFO、LRU、CLOCK、LFU 在这四条 workload 的全部 40 档容量上单调不降。换一套容量档位,被采到的下凹处数目会变,因为下凹是逐点比较的结果而不是曲线的固有属性。

警示 · 这条性质影响的是容量规划的方法,不是策略选型。0.85 个百分点的反常不足以让人放弃 SIEVE,但它意味着「加 10% 内存,命中率至少不会更差」这句话在非 stack 策略上没有保证。要判断加内存值不值,只能拿真实 trace 在目标容量附近实测,不能靠单点外推。

4 · 实测总表

容量 200、20000 次访问、四种合成 workload 下的命中率(口径见 淘汰策略是在猜未来 §5):

策略 Zipf 纯循环扫描 突发热点 热集叠加扫描
Bélády(上界) 77.2% 39.0% 91.7% 45.1%
FIFO 56.0% 0.0% 87.3% 26.3%
LRU 61.2% 0.0% 89.1% 29.9%
CLOCK 62.4% 0.0% 89.0% 31.1%
LFU 66.8% 0.0% 51.2% 38.8%
ARC 66.9% 0.0% 89.0% 40.2%
SIEVE 67.7% 0.0% 87.6% 38.8%
S3-FIFO 67.9% 0.0% 88.5% 39.6%
W-TinyLFU 68.5% 38.6% 88.7% 40.1%
Redis 近似 LRU 60.7% 0.0% 89.0% 29.6%
Redis LFU 64.3% 17.0% 88.6% 34.9%

同一组配置下每次访问的元数据写入次数:

策略 Zipf 突发热点 热集叠加扫描
FIFO 0.000 0.000 0.000
SIEVE 0.085 0.087 0.024
Redis LFU 0.082 0.083 0.042
S3-FIFO 0.171 0.101 0.063
CLOCK 0.359 0.103 0.209
Redis 近似 LRU 0.607 0.890 0.296
LRU 3.674 5.344 1.795
LFU 4.007 3.075 2.326
ARC 5.940 5.940 5.940
W-TinyLFU 8.225 9.393 6.423
图 4-1 · 全部策略在四种 workload 上的命中率矩阵,底色深浅表示与该列最优值的距离。可切换容量并按任一列排序。

5 · 判据

两张表读下来,选型的判据基本落在三个问题上。

第一个问题是写入量能不能接受。命中率的跨度是十几个百分点,写入量的跨度是两个数量级。多核高并发下每次命中都要抢锁的场景(KV 缓存、对象存储的元数据层),这一条排在最前,答案是 SIEVE 或 S3-FIFO:它们的命中率与 ARC 相当,写入量低六十倍以上。

第二个问题是流量里有没有扫描。有扫描就必须用带准入或带分段的策略。混合 workload 上 SIEVE 比 LRU 高 8.9 个百分点、比 CLOCK 高 7.7 个百分点,这个差距远大于 LRU 与 CLOCK 彼此之间的 1.2 个百分点。

第三个问题是热点会不会整批迁移。会的话就不能用无老化的 LFU:突发热点 workload 上 LFU 只有 51.2%,比 FIFO 还低 36 个百分点。带减半的 W-TinyLFU 与带衰减的 Redis LFU 都不受这一条影响。

三条之外还有一条不在表里:实现复杂度。SIEVE 是一条链表加一位 bit 加一个指针,ARC 是四个列表加一个自适应状态机。两者命中率相差不到 2 个百分点时,这一项的分量不小。

最后是本系列的适用边界。四种 workload 都是合成的,参数由本系列自己选定,因此上面所有名次都只在这套参数下成立。S3-FIFO 与 SIEVE 的论文各自在数千条生产 trace 上做过对照,那才是选型该依据的证据;本系列能提供的是机制上的理解与一套可复现的对照口径。

6 · 参考文献

  1. Bélády, L. A., Nelson, R. A., & Shedler, G. S. (1969). An anomaly in space-time characteristics of certain programs running in a paging machine. Communications of the ACM, 12(6), 349–353.
  2. Mattson, R. L., Gecsei, J., Slutz, D. R., & Traiger, I. L. (1970). Evaluation techniques for storage hierarchies. IBM Systems Journal, 9(2), 78–117.
  3. Yang, J., Zhang, Y., Qiu, Z., Yue, Y., & Vinayak, R. (2023). FIFO queues are all you need for cache eviction. SOSP 2023, 130–149.
  4. Zhang, Y., Yang, J., Yue, Y., Vigfusson, Y., & Rashmi, K. V. (2024). SIEVE is simpler than LRU: an efficient turn-key eviction algorithm for web caches. NSDI 2024, 1229–1246.