LRU-K,2Q,LIRS算法介绍与比较

    研究H2的过程中发现新的存储引擎MVStore使用了新的cache替换算法——LIRS,经过一系列相关的论文研读,发现比旧存储引擎PageStore的LRU算法改良不少。为了更好地了解LIRS的优异性,把同样属于LRU变种的基于倒数第二次访问时间对比进行cache替换的LRU-K(K一般为2)[1],2Q[2],LIRS[3]算法进行对比

概述

    为方便讨论,统一称呼要进行缓存的对象为块(或Page)。在访问块的行为中,假定存在时间局部性原理,(temporal locality - locations referenced recently likely to be referenced again)。cache替换算法就是针对局部性原理,分辨哪些是访问频率高的hot块,哪些是访问频率低的cold块,并缓存hot块到cache中,从而提高cache命中率。但对于现实中的数据存在不同的访问规律,因此cache算法为了必须尽快地适应块访问规律的改变,缓存新的hot块,并同时避免cold块“污染”hot块的缓存。

    论文[3]提出了4种块访问规律:

1.顺序访问。所有的块一个接一个被访问,不存在重访问。

2.循环访问。所有块都按照一定的间隔重复访问

3.时间密集访问。最近被访问的块是将来最有可能被访问的。

4.概率访问。所有块都有固定的访问概率,所有块都互相独立地根据概率被访问。

    论文[1]提出了2个访问规律中出现的问题:

5.Correlated References。关联访问,即块被首次访问之后,紧接着的短时间内会有数次访问。

6.Reference Retained Information Problem。访问信息保存问题。即需要在块替换出cache后,仍然保留之前的访问信息。

具体算法

    由于传统的LRU算法存在较多的问题,如顺序块访问会把hot块替换出cache,对于索引块和数据块的循环访问时,不会根据访问概率缓存索引块。LRU-K,2Q,LIRS等cache替换算法就是为了解决LRU算法的问题,提供同样甚至更高性能的同时,同时不需要外部的调控,能够自动根据块访问规律的改变对cache进行调整,都是作为通用的块缓存算法。

LRU-K

    K指的是最后第K次访问的距离,也就是倒数第K次访问时和最近一次访问的时间差。LRU-K算法主要是对比最后第K次的访问距离,访问距离越大则代表每次的访问间隔越长,因此更容易被替换出cahce。另外论文[1]中提出了对于稳定不变的访问规律,K越大,cache命中率会越高,但对于访问规律变化较大的时候,K越大则表明需要更加多的访问去适应新的规律,因此变化响应更差,因此一般取K=2。
    原论文考虑到访问规律出现5,6中的问题,提出了Correlated References Period和Reference Retained Information Period两个时间间隔参数。
    Correlated References Period,指块首次访问后的一段时间。块(可能是cold或者hot)的首次访问后可能会接着数次短时间内的关联访问,如数据库中同一事务内的select和update会多次扫描相同的块,为了避免关联访问的干扰造成对块的错误判断,在第一次访问块后,会预留在cahce中。在这段时间内的多次访问只算作一次访问。只有这段时间后块再次被访问,才算第二次被访问。
    Reference Retained Information Period,则指块被替换出cache后的一段时间。块被替换出cache后,可能很快地再次被访问,由于之前访问记录已丢弃,这样只算作首次访问,之后又很快被替换出cahce后,又再次被访问,这样又只会算作首次访问,如此下来,虽然块被频繁访问,属于hot块,但由于替换出cahce后没有保留访问信息,导致错误判断。因此对于替换出cache后的块会继续保留访问信息一段时间。
    由于原论文只给出伪代码,并没有具体的实现。虽然网络上有各种的LRU-K的实现,但某些如多个LRU栈组合的实现并不符合论文的思路。因此结合以上的讨论,个人总结了一个改进后的简单实现(K=2):

  • LRU队列A1。第一次访问的块分配cache后,插入A1队列尾部。在A1中的块被访问时,重新加入队列A1尾部。A1头部出列的块则插入优先级队列P(倒数第二次访问时间初始化为0)。该队列主要实现Correlated References Period,需要根据实际情况设置队列合理固定大小。
  • 优先级队列P。优先级队列P以倒数第二次的访问时间进行升序排序。只有当从A1出列的块或者A2重新访问的块可以插入队列P。P中的块被访问时,更新倒数第二次访问时间并重新排序。当需要分配cache的时候,P队列头部的块(倒数第二次访问时间最短,也就是距离最大)替换出cache后插入到A2中。
  • FIFO队列A2。负责保存替换出cache的块访问信息。如果A2中的块再次被访问,就更新倒数第二次访问时间,同时分配cache,插入优先级队列P。块从A2出列则删除其历史访问信息。
  • [可选]使用HashMap保存块的特证键值和对应的块访问信息,加快查找速度。
LRU-K,2Q,LIRS算法介绍与比较_第1张图片

总结
   
以上实现中,总共有3个队列,A1,A2,P。其中cache分配给在A1和P和的块,P所占cache比例较大。A2只保存块的访问信息。块的访问信息包含倒数第二次访问时间,最后一次访问时间等。如果扩展到K,则只需要通过保存K次的访问时间,同时初始化为0即可。
    LRU-K对于LRU的改进,最主要是采用了更为激进的方法去替换cold块出cache,这样能够较好地避免顺序访问对cache的影响以及能够更好地区分块访问的频率,但同时,LRU-K算法中存在一些问题:
    1.由于优先级队列的排序操作需要额外的O(logN)的时间复杂度,N为P的大小。
    2.A1,P和A2的大小都必须按照实际情况进行配置取最优比例,才能发挥最优性能。
    3.块的访问频率变化响应较慢。这是因为P的比较是按照历史的最后第K次访问距离进行比较。如果块A在P中的时候倒数第K次的距离较少,但经过较长时间才有新的访问,重新更新访问距离后,才会被快速替换出cache。

2Q

    2Q指的是Two Queue,就是依靠两个队列实现的cache替换算法。针对LRU-K算法的O(logN)时间复杂度,2Q目的是实现O(1)时间复杂度,不需要设置额外参数,并且性能等同甚至优于后者的通用cache替换算法。另外2Q算法也同样解决了LRU算法中的限制,即顺序访问,以及索引块和数据块循环访问的问题。
    论文[2]中首先提出了简化的实现方法:

  • FIFO队列A1。块首次被访问时,分配cache,插入队列A1的队尾。
  • LRU队列Am。块在A1中再次被访问时,就会加入到Am的队尾。
    分配cache时,如果cache没有空闲,首先A1超过阈值时,就会删除A1的头部,否则删除Am的头部。
    简化的实现中,A1和Am各自所占cache的比例是关键。如果A1太小,则检测是否hot块的时间太短,很可能需要较长时间才把hot块加入到Am中。但如果A1太大,则A1会占了原本所属Am的cache,hot块的数量就会减少,会影响cache命中率。
    为了解决上述问题,论文提出了2Q的完整实现,主要是把A1分割为A1in,A1out两个队列:
  • FIFO队列A1in。首次被访问的块分配cache后,插入A1in队尾。A1in的块被访问后不做任何动作。A1in队列头部出列后,替换出cache并插入块指针到A1out。A1in类似LRU-K中的A1,实现Correlated References Period,但A1in中的块被访问时不会重新插入队尾。
  • FIFO队列A1out。A1in队列头部出列后的块,只有块指针会插入到A1out队尾。A1out的块被访问后,分配cache并插入到Am队列队尾。A1out队列头部出列后,块指针被删除。
  • LRU队列Am。A1out中的块被访问后,分配cache并插入Am队尾。Am中的块被访问后,重新插入Am队尾。Am队列头部出列后,块替换出cache,相关信息被删除。
    分配cache时,如果cache没有空闲,如果A1in超出Kin阈值,A1in队列头部块出列,替换出cache后插入A1out队尾,如果A1out超过Kout阈值,A1out队列头部块出列并删除块指针;否则就把Am队列头部的块出列,替换出cache。

总结

    可以看到,和LRU-K比较最后K次访问距离,快速替换出cache中cold块相比,2Q通过对比Am的最近访问时间,替换块出cache,目的是使hot块能常驻在cache中。另外要注意到A1in和A1out两个队列的作用,A1in主要是作为Correlated References Period的实现,而A1out则是需要分辨hot块和cold块,在测试中发现A1in的块适合分配cache,A1out的块则更适合分配块指针。2Q对比LRU-K,只需要记录更少的信息,更少参数配置(推荐Kin为25%,Kout为50%),以及更低的时间复杂度O(1)。
    2Q算法中的缺点:
    1.仍然需要配置参数。A1in和A1out的大小阈值Kin和Kout的需要根据实际进行配置。
    2.Kout固定值。Kout的大小主要影响访问模式变化的响应速度,Kout为固定值则不能根据块访问模式变化而动态变化。

    3.Belady’s anomaly:cache大小增加反而导致cache命中率下降[3]。

LIRS

    LIRS,Low Inter-reference Recency Set,主要通过比较IRR(Inter-Reference Recency )来决定哪些块被替换出cache。LIRS也是目标实现一个低开销,不需要额外参数设置,并且性能优异于其它同类型的cache替换算法。
    首先要了解一下LIRS的两个概念:
recency,最近被访问的时间。
Inter-Reference Recency (IRR),同一块连续两次访问期间中间访问过的不重复块数。IRR用于记录块的历史信息,假定IRR值大的块,其值接下来也会大,也就是访问频率低。因此选择IRR大的块进行replacement,但要注意这些块的recency可能会比较低,也就是可能是最近才被访问的块。
    LIRS算法动态区分低IRR(LIR)和高IRR(HIR)的块,LIR块一般会常驻cache,HIR块则会较快被替换出cache。要保证所有LIR块都能缓存,只有比例较小的cache供HIR块缓存,当LIR块的recency超过某个值,HIR块在一个更小的recency中被访问,两者的状态就会交换。
    论文给出了详细的实现:
Stack S:  包括LIR块、少于LIR块最大recency的HIR块(包括已经缓存或者没有缓存)
Queue Q:  HIR块缓存队列,FIFO

  • 栈S大小一般没有限制,包含LIR块和HIR块的entry,entry记录了块的LIR/HIR状态,是否驻cache(LIR一定驻cache,HIR不一定)。为了加快HIR块缓存的搜索,队列Q负责连接HIR块的缓存,size为HIR块分配的缓存。当需要释放缓存时,会先删除队列Q的头部的HIR块缓存,这时如果HIR块仍然在栈S,则转换状态为非驻cache。
  • 确保栈S的底部必须为LIR块,定义“栈裁剪”操作,栈S的底部LIR块被删除,则一直删除底部块直到遇到另一个LIR块。这样做的目的是因为如果底部存在HIR块,则这些HIR块必定大于LIR块的最大recency,这样它们肯定不能转变为LIR块。
  • 如果在栈S中的HIR块被访问,则它的IRR,就是未访问前的recency,必定少于位于底部的LIR块的recency,也就是最大recency的LIR块,因此HIR块转换为LIR块,底部的LIR块则转换为HIR块,并同时从栈S删除,添加到队列Q的尾部。
  • LIR块缓存没满时,所有首次访问块都作为LIR状态,并驻cache中,直到超出LIR块缓存阈值后,首次访问块会被赋予HIR块状态。另外,栈S出栈的块都会转换为HIR状态。
    LIRS算法对于不同类型的块访问的做法如下:
  • 访问栈S中的LIR块X:LIR块必定驻cache中,所以必定命中缓存。然后把块X移动到栈S的头部,如果块X之前是在栈S的底部,则执行“栈裁剪”操作。
  • 访问驻cache中的HIR块X:访问命中缓存。把X移动到栈S头部。另外块X有两种情况:(1)块X在栈S中,把它状态转换为LIR,还删除队列Q中块X的cache。然后把栈S底部的LIR块转换为HIR块,然后移动到队列Q中。最后“栈裁剪”。(2)块X不在栈S中,则块X的状态保持HIR不变,然后从队列Q的cache移动到队列尾部。
  • 访问非驻cache中的HIR块X:没有命中缓存。首先删除队列Q头部的HIR块(如果该块在栈S,则变为非驻cache状态),这样多出cache空间,然后加载块X到该cache空间,然后移动到栈S的顶部。块X同样有两种情况:(1)块X在栈S中,改变状态为LIR,并同时改变栈底部的LIR块为HIR块,并移动到队列Q的尾部,然后“栈裁剪”。(2)块X不在栈S中,则状态为HIR,并放到队列Q的尾部。
    在上述算法中,与2Q进行对比,可以看到LIRS巧妙地把栈S作为A1in,A1out,Am的合并,通过对比块的recency从而判断IRR大小来决定块属于hot块,需要常驻cache中。另外,队列Q也解决了Reference Retained Information的问题,栈S出栈的块会重新加入队列Q一段时间。不过论文的作者显然没有考虑Correlated References
的问题,如果某些块在短时间内产生数次关联访问,则很快变为LIR块驻cache中。
    LIRS对于上面提到的4种访问模式能够快速适应。特别地,对于循环访问,LIRS能够固定开始的LIR块驻cache中,保证一定的cache命中率,这点比LRU-K以及2Q要好。另外LIRS不像2Q需要设置过多参数,通常假设LIR占99%的cache大小,HIR占1%即可。
存在问题:
    1.对于顺序访问的块,即会出现大量第一次访问块,由于栈S没有考虑到entry大小的限制,因此会一直添加这些顺序访问块到栈S的头部,使栈S变得很大。改良方法是,给栈S一个大小限制,超过的时候就去删除最接近底部的那些HIR块,这个大小可以是cache的几倍,经过测试不会造成太大的性能影响,另外栈S记录的信息只有几byte,栈S大小超过cache大小几倍不是很大问题。
    2."栈裁剪"操作只是平均的O(1)时间复杂度,并不是最差O(1)时间复杂度。

    3.对于IRR变化不会太敏感。如某些cold块IRR瞬间变小,变成LIR块,这样会把栈S底部的LIR块变为HIR块,从而很快被替换出cache,这样就造成后面的cache miss

总结

    LRU-K,2Q,LIRS三种算法都基于倒数第二次的访问时间,以此推断块的访问频率,从而替换出访问频率低的块。从空间额外消耗来看,除了LRU-K需要记录访问时间外,LIRS需要记录块状态(HIR/LIR等),2Q并不需要太多的访问信息记录,因此2Q>LIRS>LRU-K。从时间复杂度来看,LRU-K是O(logN),2Q和LIRS都是O(1),但LIRS的"栈裁剪"是平均的O(1),因此2Q>LIRS>LRU-K。从实现复杂来看,LIRS只需要两个队列,2Q和LRU-K的完整实现都需要3个队列,因此LIRS>2Q=LRU-K。最后,LIRS是唯一参数不需要去按照实际情况进行调整(尽管仍然有LIR和HIR的cache大小参数),2Q和LRU-K都需要进行细微的参数调整,因此LIRS>2Q=LRU-K。从性能角度来看,LIRS论文看得出还是有一定的提升,LIRS>2Q>LRU-K。
    本文目前只比较了三种LRU变种算法,事实上,还有基于业务情况,基于访问模式探测等不同类型的cache替换算法。另外对于LRU变种算法中,ARC也是值得探索的。我们应该明白并不存在万能的cache替换算法可以适用于任何情况。事实上,在真实database应用中,一般会对论文中的算法做适当的调整和扩展,使其更适用自身,能够发挥最佳性能。

Reference

[1]E. J. O’Neil, P. E. O’Neil, and G. Weikum, “The LRU-K Page Replacement Algorithm for Database Disk Buffering”

[2]T. Johnson and D. Shasha, “2Q: A Low Overhead High Performance Buffer Management Replacement Algorithm”

[3]Song Jiang and Xiaodong Zhang, "LIRS: An Efficient Low Inter-reference Recency Set Replacement Policy to Improve Buffer Cache Performance"

你可能感兴趣的:(Algorithm)