论文笔记:[FAST'03] ARC: A Self-Tuning, Low Overhead Replacement Cache
ARC 是一种缓存替换算法,在很多种负载环境的表现优于常用的 LRU 算法,并且实现难度和算法复杂度与 LRU 近似。
ARC 算法具有以下优良特性:
- 在 recency 和 frequency 之间持续的进行动态(在线)调整
- 无需事先指定特别的参数(先验知识)
- 具有全局优化策略(意译,不确定翻译的对不对,原文 empirically universal)
- 可以(在某种程度上)抵抗线性扫描(scan-resistant)
这篇文章中还存在一点问题没来的及修改,L1和L2不是固定大小为c,而是尽可能的维护L1和L2的大小为c,将cache项从L1中挪到L2的过程中会减小L1增加L2。
这篇文章中的公式较多,建议去我的博客阅读:论文笔记:[FAST'03] ARC: A Self-Tuning, Low Overhead Replacement Cache
原论文中花费了很多篇幅介绍之前人们试图提出比 LRU 更好的算法的一些工作,并进行了一番对比,本文限于篇幅和精力略过这部分内容。
基本思想
LRU 的问题在于其只考虑了 recency 而完全没有考虑 frequency。一般而言,保存 freqency 信息的代价又比较大。ARC 的最根本的思路(以下称为 DBL)在于使用 2 个 LRU,其中一个 L1存放最近被访问 1 次的数据(也就是传统的 LRU),另一个 L2 存放最近被访问 2 次及以上的数据。这一近似减少了维护 frequency 的成本。最终目标是维持 |L1|=|L2|=c,但是论文中并没有解释为什么要这么做。
我们把整个 Cache 分为以下 2 个部分:
- Cache Directory
- Cache Item
其中 Cache Directory 用于进行索引,Cache Item 真正表示在缓存中的内容。对于传统 LRU 算法而言,以上 2 者的大小是一样的(Directory 索引的项的数量和真正在缓存中的内容的数量)。对于上面提到的 DBL 而言,以上 2 者的大小也是一样的,为 |L1|+|L2|=2c。特别的,在 DBL 种,L1 就是一个传统的 LRU,其大小为 c。
上面提到的 DBL 算法的问题是使用了 2 倍于 LRU 的空间,其效果比 LRU 好是显然的,能不能使用和 LRU 一样多的空间得到更好的效果呢?一个直接的想法是使用 2c的 Cache Directory 但是只保留 c 个 Cache Item 常驻内存,那么在这 2c 个索引项中如何取舍就是一个问题。如 [figure-1] 所示,我们将 L1 和 L2 分别拆分为 T1,B1,T2,B2。直觉上,T1 比 B1 更有价值,T2 比 B2 更有价值,如果我们能够维持 |T1|+|T2|≤c,则应当将 T1∪T2中的元素保留在 Cache 中。

剩下的问题,在于如何在 L1∪L2中调节 T1∪T2 的位置(即调节 |B1| 和 |B2|),以及如何在 T1∪T2 中调节两者的大小分配。特别的,当 |L1|=|T1|=c时,该算法等价于 LRU,也就是说如果调节得当的话,该算法至少能做到和 LRU 一样好。
ARC 算法
由于我们要求保持性质 |L1|=|L2|=c,因此当调节 T1 和 T2 的关系时,实际上就调节了 T1∪T2 在 L1∪L2 中的位置。直觉上相当于在 L1 和 L2 中间的地方有一个固定大小的滑块,我们在调节这个滑块的位置。不妨令 |T1|=p,则 |B1|=|T2|=c−p 且 |B2|=p。
接下来,我们需要思考一下 B1 和 B2 对我们而言意味着什么。回想上面提到的 DBL,L1 意味着最近只命中了 1 次的缓存项索引,其中 T1 是我们保留在缓存中的内容,B1 是我们没有保留在缓存中的内容。因此,如果查询请求命中了 B1,意味着我们应当把这次命中当作命中了 L1 来看,并且相应的,说明我们的 |T1| 可能小了,没能将这次命中的数据存起来。论文中,当 |B1|>|B2| 时,p 增加 1,当 |B1|<|B2| 时,p 增加 |B2|/|B1|,但是论文中没有解释为什么这么设计。
完整的 ARC 算法如 [figure-2] 所示,其中 Case I-III 对应于 DBL 命中的情况,Case IV 对应于 DBL 未命中的情况。

问题回顾
之前提到了一些问题,在此继续探讨一下。
L1 和 L2 的大小是否可以调整
在目前的设计中,|L1|=|L2|=c,由于我们最多只保留 |T1|+|T2|=c 项内容,在极端情况下 |L1|=|T1|=c,此时进一步增大 L1 也不能再进行什么调整了(受限于 Cache 大小),从这个角度来看在 L1 和 L2 之间进行调整可能是没什么意义的。
但是在论文中特别提到了 E. Extra History,讨论了这样一个问题:
An interesting question is whether we can incorporate more history information in ARC to further improve it.
论文中给出了一种利用额外的空间的方法,似乎暗示了这么做是有用的,但是没有论证对比这一方法的效果如何。
p 的调整为什么不是固定的
推测是因为当 B1 小的时候命中 B1 是比较困难的,要对此做一个加成。但是为什么这么做论文并没有给出一个解释,也没有什么对比。
Scan-Resistant
直觉上 ARC 是可以抵御扫描 Pattern 的请求的,因为 ARC 不仅考虑了 recency,还考虑了 frequency。具体而言,有以下 2 个原因:
- 扫描会刷新 L1 的内容,但是不会影响 L2 的内容
- 扫描会更多的命中 B2(相较于 B1),因此会进一步减小 |T1|,进而使得扫描造成的影响比单纯考虑第 1 点更小
不总结总觉得少点什么
总的来说,ARC 算法通过一种比较巧妙的方法,在不显著增加实现成本和算法时间复杂的情况下,较为显著的改进了 LRU 算法。使用 LRU 来承载最近遇到 2 次以上的数据来近似捕获 frequency 信息是一大亮点。动态平衡的方案直觉上有效,但是细节上缺乏有力的论证,不确定是不是一个最优的算法。对于动态变化的数据而言,动态平衡的算法优于静态的算法是可以预期的。
References
- [1] MEGIDDO N, MODHA D S. ARC: A Self-Tuning, Low Overhead Replacement Cache[C]//CHASE J. Proceedings of the {FAST} ’03 Conference on File and Storage Technologies, March 31 - April 2, 2003, Cathedral Hill Hotel, San Francisco, California, {USA}. USENIX, 2003.
- [2] NIMROD MEGIDDO, MODHA D S. one up on LRU[J]. ;Login:, 2003, 28(4).