2025年3月24日 星期一 甲辰(龙)年 月廿三 设为首页 加入收藏
rss
您当前的位置:首页 > 计算机 > 编程开发 > Go语言

golang-lru包学习 - ARC算法

时间:03-22来源:作者:点击数:4
城东书院 www.cdsy.xyz

ARC算法

代码:ARC

Wiki:Adaptive replacement cache

论文:A low overhead high performance buffer replacement algorithm

ARC算法的描述

ARC算法是为了整个LFU和LRU两种替换算法的优点,实现他们之间的自适应的workload调整。算法把有效缓存队列划分为两个,T1和T2,其中T1是LRU队列用于保存最近使用的条目、T2是LFU队列用于保存最常使用的条目。 他们各自都包含了一个名为ghost list的淘汰队列,分别命名为B1、B2,但tracking操作(即外部添加、查找操作)只针对T1、T2进行.ARC算法会根据缓存命中情况自动调整T1、T2的大小,以保证整个缓存既定长度的恒定。当新元素较多的时候,T1长度会增长T2长度会缩小,而当旧元素命中较多时候 T2长度会增长T1长度会缩小。

理论ARC算法核心结构:

  • T1, 用于保存最近的条目
  • T2, 用于保存最常用的条目,至少被引用两次
  • B1, 用于保存从T1淘汰的条目
  • B2, 用于保存从T2淘汰的条目
  • . . . [ B1 <-[ T1 <-!-> T2 ]-> B2 ] . .
  • [ . . . . [ . . . . . . ! . .^. . . . ] . . . . ]
  • [ fixed cache size (c) ]
  • [ L ]
  • L可视为一个有效的缓存队列整体,其长度恒定不变。通过T1T2长度的变化改变偏重P的大小。
  • 首次添加 Add("A")
  • | T1(LRU) | | B1(LRU) | | T2 (LFU)| | B2 (LFU)|
  • |----------| |----------| |---------| |---------|
  • ele(A)-> | ele(A) | | | | | | |
  • |----------| |----------| |---------| |---------|
  • 第二次添加或者访问 Add("A") / Get("A")
  • | T1(LRU) | | B1(LRU) | | T2 (LFU)| | B2 (LFU)|
  • |----------| |----------| |---------| |---------|
  • | | | | | ele(A) | | |
  • |----------| |----------| |---------| |---------|
  • 对于已被T1或者T2淘汰的数据,再次添加 Add("A"),会直接进入T2
  • | T1(LRU) | | B1(LRU) | | T2 (LFU)| | B2 (LFU)|
  • |----------| |----------| |---------| |---------|
  • | | | ele(A) | | | | |
  • |----------| |----------| |---------| |---------|
  • | T1(LRU) | | B1(LRU) | | T2 (LFU)| | B2 (LFU)|
  • |----------| |----------| |---------| |---------|
  • | | | | | ele(A) | | |
  • |----------| |----------| |---------| |---------|

理论ARC算法替换过程

新元素: + 若空间不足,淘汰T2 + 添加新元素到T1

已存在元素: + 若在B1或B2存在,移动到T2 + 若空间不足,淘汰T1

查询命中 + 若T1查询命中,移动到T2

算法实现:

本包跟2Q算法实现类似,将T2用LRU队列来实现。其结构如下图所示。T1,T2,B1,B2依然按照理论ARC中的设计,区别只是T2 B2用的是LRU算法而不是LFU。

本包算法实现结构:

  • | T1(LRU) | | B1(LRU) | | T2 (LRU)| | B2 (LRU)|
  • |----------| |----------| |---------| |---------|
  • ele(A)-> | ele(A) | | | | | | |
  • |----------| |----------| |---------| |---------|

ARC算法结构对象:

  • type ARCCache struct {
  • size int // 整体缓存的既定长度
  • p int // P T1和T2的侧重值
  • t1 simplelru.LRUCache // T1 最近使用队列
  • b1 simplelru.LRUCache // B1 T1淘汰队列
  • t2 simplelru.LRUCache // T2 最常使用队列
  • b2 simplelru.LRUCache // B2 T2淘汰队列
  • lock sync.RWMutex // 读写锁
  • }

函数

  • type ARCCache interface {
  • NewARC(size int) (*TwoQueueCache, error) // 创建一个ARC算法的缓存对象
  • Add(key, value interface{}) // 添加元素,若元素不存在则添加元素,若元素存在则更新元素
  • Get(key interface{}) (value interface{}, ok bool) // 获取元素
  • Contains(key interface{}) (ok bool) // 查看该元素是否存在,但不会更新元素位置
  • Peek(key interface{}) (value interface{}, ok bool) // 获取元素,但不会更新元素位置
  • Remove(key interface{}) bool // 删除元素
  • Len() int // 返回队列中元素数量
  • Purge() // 清除队列中全部元素
  • }

过程

ARC算法核心过程是Add(),在Add()方法中需要针对不同情况处理四个队列的淘汰问题和T1、T2长度问题,最后更新偏重值P。

Add() 添加元素

  • func (c *ARCCache) Add(key, value interface{}) {
  • c.lock.Lock()
  • defer c.lock.Unlock()
  • if c.t1.Contains(key) { // 若在T1存在,移动到T2
  • c.t1.Remove(key)
  • c.t2.Add(key, value)
  • return
  • }
  • if c.t2.Contains(key) { // 若在T2存在,更新值
  • c.t2.Add(key, value)
  • return
  • }
  • if c.b1.Contains(key) { // 若在B1存在,移动到T2
  • delta := 1 // 侧重增量
  • b1Len := c.b1.Len()
  • b2Len := c.b2.Len()
  • if b2Len > b1Len {
  • delta = b2Len / b1Len
  • }
  • if c.p+delta >= c.size { // 调整侧重,靠近T1这边
  • c.p = c.size
  • } else {
  • c.p += delta
  • }
  • if c.t1.Len()+c.t2.Len() >= c.size {
  • c.replace(false) // 淘汰T2,缓存窗口往T1移动
  • }
  • c.b1.Remove(key)
  • c.t2.Add(key, value)
  • return
  • }
  • if c.b2.Contains(key) { // 若在B2存在,移动到T2
  • delta := 1 // 侧重增量
  • b1Len := c.b1.Len()
  • b2Len := c.b2.Len()
  • if b1Len > b2Len {
  • delta = b1Len / b2Len
  • }
  • if delta >= c.p { // 调整侧重,靠近T2这边
  • c.p = 0
  • } else {
  • c.p -= delta
  • }
  • if c.t1.Len()+c.t2.Len() >= c.size {
  • c.replace(true) // 淘汰T1,缓存窗口往T2移动
  • }
  • c.b2.Remove(key)
  • c.t2.Add(key, value)
  • return
  • }
  • // T1 T2 B1 B2 都无该元素,添加到T1
  • if c.t1.Len()+c.t2.Len() >= c.size { // 超出整体缓存既定大小
  • c.replace(false) // T2淘汰
  • }
  • if c.b1.Len() > c.size-c.p {
  • c.b1.RemoveOldest()
  • }
  • if c.b2.Len() > c.p {
  • c.b2.RemoveOldest()
  • }
  • c.t1.Add(key, value)
  • return
  • }

Get方法与2Q相似,T1命中的则移动到T2队列

Get() 获取元素

  • func (c *ARCCache) Get(key interface{}) (value interface{}, ok bool) {
  • c.lock.Lock()
  • defer c.lock.Unlock()
  • if val, ok := c.t1.Peek(key); ok { // 若在T1存在,移动到T2
  • c.t1.Remove(key)
  • c.t2.Add(key, val)
  • return val, ok
  • }
  • if val, ok := c.t2.Get(key); ok {
  • return val, ok
  • }
  • // No hit
  • return nil, false
  • }

总结

  • ARC算法最大优势是自动调整两种队列的负载,也无需像2Q算法那样需要设置参数
  • ARC算法可减弱遍历访问造成的缓存污染
城东书院 www.cdsy.xyz
方便获取更多学习、工作、生活信息请关注本站微信公众号城东书院 微信服务号城东书院 微信订阅号
推荐内容
相关内容
栏目更新
栏目热门
本栏推荐