ARC算法是2003年提出的缓存替换算法,是众多针对LRU算法的改良算法之一。
本文仅从模拟实现角度分析ARC算法,可以说就是解释ARC算法的内容,而不会将重点放在ARC算法的原理和解释其优越性上,同时代码实现也仅可用于模拟,不是针对具体应用。
ARC的结构是两个LRU队列,我们称其为 L 1 L1 L1和 L 2 L2 L2, L 1 L1 L1存储首次被访问的页,而 L 2 L2 L2存储被访问过两次及以上的页。当然这里被访问两次及以上是指在被从这两个LRU队列中淘汰之前再次被访问,因为从两个LRU队列中淘汰的页的访问就不会再被保留了。 t1<p
假设我们的缓存大小为 c c c,那么两个队列总共的最大长度为 2 c 2c 2c, L 1 L1 L1队列的最大长度为 c c c,而 L 2 L2 L2在不超过最大总长度的情况下可以侵占 L 1 L1 L1的空间。自然,我们缓存不会将两个LRU中的页全部存储。ARC只存储两个LRU队列的前面一部分(假设我们把MRU一端称为前面/top,LRU一端称为后面/bottom),只有这一部分是存储具体内容的,而尾端只有页号,没有内容,不占用太多的空间。具体是前几个呢?ARC算法给出了精细的调控。
我们把 L 1 L1 L1中保存的部分称为 T 1 T1 T1( T 2 T2 T2 for L 2 L2 L2), T 1 T1 T1的长度记为 t 1 t1 t1( t 2 t2 t2 for T 2 T2 T2)。ARC为 T 1 T1 T1的长度设置了一个目标大小 p p p, 0 ≤ p ≤ c 0\leq p\leq c 0≤p≤c,这个 p p p代表了ARC更倾向于保留初次访问的数据还是再次访问的数据。当 t 1 > p t1>p t1>p时,我们优先淘汰 T 1 T1 T1中的页,当 t 1 < p t1
以上其实是给定 p p p的算法的简单描述,作者将这个算法称作FRC算法(Fixed Replacement Cache),那么我们的 p p p是怎么确定的呢,这就是ARC算法Adaptive的地方了。接下来我开始具体分析ARC算法。
我们可以想象两个LRU队列,在T部和B部的分界处有一块可移动的隔板,在最前端有一个可插入的缝隙。
这两个隔板之间的部分就是存储在缓存中的部分,而缝隙就是最近一次访问的页可能插入的位置。一方面,两个隔板之间的最大距离不可能超过缓存的大小,即 c c c;另一方面,我们只考虑隔板向自身队列的头部(top端)移动,在算法中不会将隔板向尾部移动,因为隔板 b 1 b1 b1向头部移动意味着 T 1 T1 T1的LRU页从缓存中删除,滚向 B 1 B1 B1,这是可能的,而隔板向尾部移动则代表 B 1 B1 B1的MRU页变成 T 1 T1 T1的LRU页,这是不可能的,这种移动没有合理性。
隔板的移动会改变缓存的长度,不会改变 L 1 L1 L1或者 L 2 L2 L2的长度;而在两个缝隙之一插入元素,则既会改变 L 1 , L 2 L1,L2 L1,L2的长度也会改变缓存的长度。除了以上两个操作,当然还有第三个和第四个操作,那就是从 L 1 , L 2 L1,L2 L1,L2的尾端淘汰元素的操作以及把两个队列中被访问的元素删除的操作(要移到MRU位置去)。
再次总结一下我们可使用的四种操作:
1)将两个隔板之一向尾部移动一个单位
2)在两个缝隙之一插入新访问的页
3)从两个LRU队列之一的尾部淘汰一个页;
4)(如果新访问的页在缓存中)从 L 1 ∪ L 2 L1 \cup L2 L1∪L2中删去被访问的元素
这三个操作通过协调保证了:
1) 两个隔板之间的间距不超过 c c c(缓存大小限制)
2) 0 ≤ l 1 ≤ c , 0 ≤ l 1 + l 2 ≤ 2 c 0\leq l1\leq c, 0\leq l1 + l2\leq 2c 0≤l1≤c,0≤l1+l2≤2c(定义)
3)给定 p p p,ARC的淘汰和插入行为与FRC一致
初始状态下, T 1 , 2 , B 1 , 2 T1,2,B1,2 T1,2,B1,2都是空的,目标大小 p = 0 p=0 p=0。ARC对访问 x x x分以下几类:
c a s e I case\, I caseI: x x x 在缓存 T 1 ∪ T 2 T1 \cup T2 T1∪T2 中:
这种情况为ARC算法的命中。不会涉及页的淘汰,并且由于删除的页和插入都发生在缓存内,更在两个队列内,所以既不会改变缓存的长度,也不会导致两个队列总长度变化。我们只需要用操作4和操作2即可。其中插入一定是在 T 2 T2 T2的MRU位置。
c a s e I I case\, II caseII: x x x在 B 1 B1 B1或者 B 2 B2 B2中:
如果在B1/B2中,我们知道这个页会被重新读取,从B1/B2中删除,并且移动到T2的MRU位置,也就是插入T2的缝隙。这个操作同样不会改变队列总长度,不会增大L1的长度,但是这样会导致缓存增大,就有潜在的超出缓存的风险。这里,在没有对缓存是否超出 c c c进行判断的情况下,ARC依然会进行操作1),移动隔板,以使缓存大小减1,与操作2)效果抵消。进行操作1)的时候我们就要注意FRC的规则了,该把 T 1 T1 T1还是 T 2 T2 T2的LRU元素移动到 B 1 B1 B1或 B 2 B2 B2中去呢?这是要根据 x x x在 B 1 B1 B1还是 B 2 B2 B2中、 T 1 T1 T1长度与 p p p的关系来共同确定的。
不仅如此,ARC与FRC的区别在于会根据miss的页在 B 1 B1 B1还是在 B 2 B2 B2中调整p的大小,如果在 B 1 B1 B1中,会将 p p p向更加偏向 L 1 L1 L1的方向调整,即增大 p p p,反之则会减小 p p p。
总之,将会发生的事情是:先调整 p p p,然后移动隔板(两个队列的长度不变,缓存长度-1)删除被访问的页,移动到 T 2 T2 T2的MRU位置(两个队列总长度不变,缓存长度+1)。
如图,adaption就是调整 p p p,replace就是移动隔板。
c a s e I I I case\, III caseIII: x x x不在任何一个队列中。最后一种情况是完完全全的miss,不会涉及调整 p p p,只会涉及淘汰和插入。首先这次插入一定发生在 T 1 T1 T1的MRU位置,因为这是初次访问的页。注意到前面三种情况都不会涉及到队列总长度的变化,而且插入都发生在 T 2 T2 T2,也就是说 L 1 L1 L1的长度不会增加。而这种情况不仅会导致总长度增加,并且会导致 L 1 L1 L1的长度增加。因此在这种情况下,作者对队列长度进行了讨论:
C a s e A Case\,A CaseA: L 1 L1 L1长度已达到 c c c:
这个时候我们就要从 L 1 L1 L1的尾端淘汰一个页(彻彻底底的淘汰),这样才能在 L 1 L1 L1头部插入新的页(同时,这一操作自然也使总长度减少了)。这一操作也被作者分了两类来讨论,即 L 1 L1 L1中的 B 1 B1 B1是否为空的情况。如果 B 1 B1 B1为空的话,删除的就是 T 1 T1 T1的最后一个元素,否则删除的是 B 1 B1 B1的最后一个元素。反正都是删除 L 1 L1 L1的最后一个元素,这个元素在 B 1 B1 B1还是在 T 1 T1 T1又有什么分别呢?答案是如果在 B 1 B1 B1,那么维持 L 1 L1 L1长度的同时, T 1 T1 T1的长度却增加了,这样我们就不得不考虑FRC规则了,如果这个时候 T 1 T1 T1长度是超过 p p p的,那么我们要调整隔板,放一个 T 1 T1 T1尾部的元素到 B 1 B1 B1中,以使 T 1 T1 T1的长度更接近 p p p。这就是当 L 1 L1 L1长度已到达 c c c时的操作。
C a s e B Case\,B CaseB:如果L1没有达到c:
这与case A的区别就是,我们不一定要淘汰L1中的元素了,可能不淘汰(如果总长度不会超过 2 c 2c 2c),或者淘汰 L 2 L2 L2中的元素:
这里的判断条件 ∣ T 1 ∣ + ∣ T 2 ∣ + ∣ B 1 ∣ + ∣ B 2 ∣ ≥ c |T1|+|T2|+|B1|+|B2|\geq c ∣T1∣+∣T2∣+∣B1∣+∣B2∣≥c,其实就等于说 ∣ T 1 ∣ + ∣ T 2 ∣ = c |T1| + |T2| = c ∣T1∣+∣T2∣=c。为什么呢,我们可以通过反证来证明:即 ∣ T 1 ∣ + ∣ T 2 ∣ < c |T1|+|T2|
我们可以看到这个证明很关键的一点在于 B 1 , 2 B1,2 B1,2初始为空。
在理解了这个条件以后,算法就很简单了。上面操作的含义就是,如果缓存已满,那么我们需要减少缓存中的元素,这通过移动隔板实现(REPLACE)。进一步的,如果两个LRU的总长度已满( ∣ T 1 ∣ + ∣ T 2 ∣ + ∣ B 1 ∣ + ∣ B 2 ∣ = 2 c |T1| + |T2| +|B1| +|B2|= 2c ∣T1∣+∣T2∣+∣B1∣+∣B2∣=2c),那么我们需要减少整个队列的长度,也就是通过淘汰 L 2 L2 L2的队尾元素。这时,注意 ∣ T 1 ∣ + ∣ T 2 ∣ = c |T1|+|T2| = c ∣T1∣+∣T2∣=c故 ∣ B 1 ∣ + ∣ B 2 ∣ = c |B1|+|B2| = c ∣B1∣+∣B2∣=c,又 ∣ B 1 ∣ ≤ ∣ B 1 ∣ + ∣ T 1 ∣ ≤ c |B1|\leq|B1|+|T1|\leq c ∣B1∣≤∣B1∣+∣T1∣≤c故 ∣ B 2 ∣ > 0 |B2| > 0 ∣B2∣>0,即B2非空。所以算法明确写出是淘汰B2的队尾元素。
写到这里,可以确认算法确实是严密并且完整的,并没有省略步骤。其实上面的分析也只是为了验证这一点而已。下面可以代码实现了。
import collections
class ARC_buffer:
def __init__(self, c):
self.c = c
self.p = 0.0
self.T1 = collections.OrderedDict()
self.T2 = collections.OrderedDict()
self.B1 = []
self.B2 = []
def require(self,access):
# get required page from secondary storage
return 1
def visit(self, access):
# print("access:{}".format(access))
if access in self.T1:
hit = True
result = self.T1.pop(access)
self.T2[access] = result
# print("hit in T1!")
elif access in self.T2:
hit = True
result = self.T2.pop(access)
self.T2[access] = result
# print("hit in T2!")
elif access in self.B1:
# print("found in B1!")
hit = False
delta = max(1, len(self.B2)/len(self.B1))
self.p = min(self.p+delta, self.c)
self.replace(access)
self.B1.remove(access)
result = self.require(access)
self.T2[access] = result
elif access in self.B2:
# print("found in B2!")
hit = False
delta = max(1, len(self.B1)/len(self.B2))
self.p = max(self.p - delta, 0)
self.replace(access)
self.B2.remove(access)
result = self.require(access)
self.T2[access] = result
elif len(self.T1) + len(self.B1) == self.c:
# print("new visit but L1 is full!")
hit = False
if len(self.T1) < self.c:
# print("B1 not empty, evict from B1!")
self.B1.pop()
self.replace(access)
else:
# print("B1 empty, evict from T1!")
self.T1.popitem(last=False)
result = self.require(access)
self.T1[access] = result
elif len(self.T1) + len(self.T2) + len(self.B1)+ len(self.B2) >= self.c:
if len(self.T1) + len(self.B1) > self.c:
# print("error!L1 exceeds c!")
hit = False
if len(self.T1)+len(self.T2) != self.c:
# print("L1 + L2 >= c while cache is not full!")
else:
# print("cache full!")
if len(self.T1) + len(self.T2) + len(self.B1) + len(self.B2) == 2*self.c:
# print("L1 + L2 is 2c!")
self.B2.pop()
self.replace(access)
result = self.require(access)
self.T1[access] = result
else:
hit = False
result = self.require(access)
self.T1[access] = result
"""
T1 = str([key for key in self.T1])
if len(self.B1) > 0:
B1 = str([key for key in self.B1].reverse())
else:
B1 = "[]"
if len(self.T2) > 0:
T2 = str([key for key in self.T2].reverse())
else:
T2 = "[]"
B2 = str([key for key in self.B2])
title = len(B1)//2*" "+"B1"+(len(T1)+len(B1)+1)//2*" "+"T1"+\
(len(T1)+len(T2)+1)//2*" "+"T2"+(len(T2)+len(B2)+1)//2*" "+"B2"+len(B2)//2*" "
print(title)
print(B1 + " " + T1 + " " + T2 + " " + B2 +"\n")
"""
return hit
def replace(self, access):
if len(self.T1) > 0 and (len(self.T1) > self.p or (access in self.B2 and len(self.T1) == self.p)):
x, result = self.T1.popitem(last=False)
self.B1.insert(0, x)
print("evict {} from T1 to B1".format(x))
else:
x, result = self.T2.popitem(last=False)
self.B2.insert(0, x)
print("evict {} from T2 to B2".format(x))
def ARC(self, H):
hit = 0
count = 0
for access in H:
if self.visit(access):
hit += 1
count += 1
return hit, count # 统计命中次数与访问总数