ARC算法分析与实现

ARC算法分析与实现

ARC算法是2003年提出的缓存替换算法,是众多针对LRU算法的改良算法之一。
本文仅从模拟实现角度分析ARC算法,可以说就是解释ARC算法的内容,而不会将重点放在ARC算法的原理和解释其优越性上,同时代码实现也仅可用于模拟,不是针对具体应用。

FRC

ARC的结构是两个LRU队列,我们称其为 L 1 L1 L1 L 2 L2 L2 L 1 L1 L1存储首次被访问的页,而 L 2 L2 L2存储被访问过两次及以上的页。当然这里被访问两次及以上是指在被从这两个LRU队列中淘汰之前再次被访问,因为从两个LRU队列中淘汰的页的访问就不会再被保留了。
假设我们的缓存大小为 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 0pc,这个 p p p代表了ARC更倾向于保留初次访问的数据还是再次访问的数据。当 t 1 > p t1>p t1>p时,我们优先淘汰 T 1 T1 T1中的页,当 t 1 < p t1

t1<p时,我们优先淘汰 T 2 T2 T2中的页,总之,是让 T 1 T1 T1的长度稳定在 p p p附近(注意,新插入的页在 T 1 T1 T1还是 T 2 T2 T2中是由它是否是初次访问决定的,因此我们只能通过选择淘汰哪一个列表的页来调整 T 1 T1 T1的长度)。而当 t 1 = p t1=p t1=p时,作者表示这个时候的淘汰策略可以任意一点,如果miss页在 B 1 B1 B1中则淘汰 T 2 T2 T2中的页,否则淘汰 T 1 T1 T1中的页。
以上其实是给定 p p p的算法的简单描述,作者将这个算法称作FRC算法(Fixed Replacement Cache),那么我们的 p p p是怎么确定的呢,这就是ARC算法Adaptive的地方了。接下来我开始具体分析ARC算法。

ARC算法中包含的操作

我们可以想象两个LRU队列,在T部和B部的分界处有一块可移动的隔板,在最前端有一个可插入的缝隙。ARC算法分析与实现_第1张图片
这两个隔板之间的部分就是存储在缓存中的部分,而缝隙就是最近一次访问的页可能插入的位置。一方面,两个隔板之间的最大距离不可能超过缓存的大小,即 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 L1L2的长度也会改变缓存的长度。除了以上两个操作,当然还有第三个和第四个操作,那就是从 L 1 , L 2 L1,L2 L1L2的尾端淘汰元素的操作以及把两个队列中被访问的元素删除的操作(要移到MRU位置去)。
再次总结一下我们可使用的四种操作:
1)将两个隔板之一向尾部移动一个单位
2)在两个缝隙之一插入新访问的页
3)从两个LRU队列之一的尾部淘汰一个页;
4)(如果新访问的页在缓存中)从 L 1 ∪ L 2 L1 \cup L2 L1L2中删去被访问的元素
这三个操作通过协调保证了:
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 0l1c,0l1+l22c(定义)
3)给定 p p p,ARC的淘汰和插入行为与FRC一致

ARC如何使用这三种操作

初始状态下, T 1 , 2 , B 1 , 2 T1,2,B1,2 T12B12都是空的,目标大小 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 T1T2 中:
这种情况为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)。
ARC算法分析与实现_第2张图片
如图,adaption就是调整 p p p,replace就是移动隔板。
ARC算法分析与实现_第3张图片

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时的操作。
ARC算法分析与实现_第4张图片

C a s e   B Case\,B CaseB:如果L1没有达到c:
这与case A的区别就是,我们不一定要淘汰L1中的元素了,可能不淘汰(如果总长度不会超过 2 c 2c 2c),或者淘汰 L 2 L2 L2中的元素:
ARC算法分析与实现_第5张图片
这里的判断条件 ∣ T 1 ∣ + ∣ T 2 ∣ + ∣ B 1 ∣ + ∣ B 2 ∣ ≥ c |T1|+|T2|+|B1|+|B2|\geq c T1+T2+B1+B2c,其实就等于说 ∣ T 1 ∣ + ∣ T 2 ∣ = c |T1| + |T2| = c T1+T2=c。为什么呢,我们可以通过反证来证明:即 ∣ T 1 ∣ + ∣ T 2 ∣ < c |T1|+|T2|T1+T2<c ∣ T 1 ∣ + ∣ T 2 ∣ + ∣ B 1 ∣ + ∣ B 2 ∣ ≥ c |T1|+|T2|+|B1|+|B2|\geq c T1+T2+B1+B2c,那么 B 1 , 2 B1,2 B12一定非空。但一开始 B 1 , 2 B1,2 B12都是空的,让 B 1 , 2 B1,2 B12非空的操作只有移动隔板,移动隔板只出现在“x in B1”、“x in B2”和“ ∣ T 1 ∣ + ∣ T 2 ∣ + ∣ B 1 ∣ + ∣ B 2 ∣ ≥ c |T1|+|T2|+|B1|+|B2|\geq c T1+T2+B1+B2c”这三种情况下。在 B 1 , 2 B1,2 B12为空的情况下,前两种情况都不会达到,所以只有通过第三种情况让 B 1 , 2 B1,2 B12非空。这时就会在 B 1 , 2 B1,2 B12为空的条件下达到 ∣ T 1 ∣ + ∣ T 2 ∣ + ∣ B 1 ∣ + ∣ B 2 ∣ ≥ c |T1|+|T2|+|B1|+|B2|\geq c T1+T2+B1+B2c。换句话说,第一次达到 ∣ T 1 ∣ + ∣ T 2 ∣ + ∣ B 1 ∣ + ∣ B 2 ∣ ≥ c |T1|+|T2|+|B1|+|B2|\geq c T1+T2+B1+B2c这个条件时,一定是 B 1 , 2 B1,2 B12为空的。又因为我们从来没有减少过缓存的内容:我们移动隔板的操作一定是在插入的同时,而淘汰都是淘汰 B o t t o m Bottom Bottom部分的元素。所以一旦达到 ∣ T 1 ∣ + ∣ T 2 ∣ = c |T1| + |T2| = c T1+T2=c ∣ T 1 ∣ + ∣ T 2 ∣ |T1| + |T2| T1+T2就会一直稳定在 c c c了。
我们可以看到这个证明很关键的一点在于 B 1 , 2 B1,2 B12初始为空。
在理解了这个条件以后,算法就很简单了。上面操作的含义就是,如果缓存已满,那么我们需要减少缓存中的元素,这通过移动隔板实现(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 B1B1+T1c ∣ 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  # 统计命中次数与访问总数




你可能感兴趣的:(代码分享,数据库)