| package lru |
| |
| import ( |
| "sync" |
| |
| "google3/third_party/golang/lru/simplelru/simplelru" |
| ) |
| |
| // ARCCache is a thread-safe fixed size Adaptive Replacement Cache (ARC). |
| // ARC is an enhancement over the standard LRU cache in that tracks both |
| // frequency and recency of use. This avoids a burst in access to new |
| // entries from evicting the frequently used older entries. It adds some |
| // additional tracking overhead to a standard LRU cache, computationally |
| // it is roughly 2x the cost, and the extra memory overhead is linear |
| // with the size of the cache. ARC has been patented by IBM, but is |
| // similar to the TwoQueueCache (2Q) which requires setting parameters. |
| type ARCCache struct { |
| size int // Size is the total capacity of the cache |
| p int // P is the dynamic preference towards T1 or T2 |
| |
| t1 *simplelru.LRU // T1 is the LRU for recently accessed items |
| b1 *simplelru.LRU // B1 is the LRU for evictions from t1 |
| |
| t2 *simplelru.LRU // T2 is the LRU for frequently accessed items |
| b2 *simplelru.LRU // B2 is the LRU for evictions from t2 |
| |
| lock sync.RWMutex |
| } |
| |
| // NewARC creates an ARC of the given size |
| func NewARC(size int) (*ARCCache, error) { |
| return NewARCWithEvict(size, nil) |
| } |
| |
| // NewARCWithEvict creates an ARC of the given size with onEvict callback. |
| func NewARCWithEvict(size int, onEvicted func(key interface{}, value interface{})) (*ARCCache, error) { |
| callback := simplelru.EvictCallback(onEvicted) |
| // Create the sub LRUs |
| b1, err := simplelru.NewLRU(size, nil) |
| if err != nil { |
| return nil, err |
| } |
| b2, err := simplelru.NewLRU(size, nil) |
| if err != nil { |
| return nil, err |
| } |
| t1, err := simplelru.NewLRU(size, callback) |
| if err != nil { |
| return nil, err |
| } |
| t2, err := simplelru.NewLRU(size, callback) |
| if err != nil { |
| return nil, err |
| } |
| |
| // Initialize the ARC |
| c := &ARCCache{ |
| size: size, |
| p: 0, |
| t1: t1, |
| b1: b1, |
| t2: t2, |
| b2: b2, |
| } |
| return c, nil |
| } |
| |
| // Get looks up a key's value from the cache. |
| func (c *ARCCache) Get(key interface{}) (value interface{}, ok bool) { |
| c.lock.Lock() |
| defer c.lock.Unlock() |
| |
| // If the value is contained in T1 (recent), then |
| // promote it to T2 (frequent) |
| if val, ok := c.t1.Peek(key); ok { |
| c.t1.RemoveWithoutEvict(key) |
| c.t2.Add(key, val) |
| return val, ok |
| } |
| |
| // Check if the value is contained in T2 (frequent) |
| if val, ok := c.t2.Get(key); ok { |
| return val, ok |
| } |
| |
| // No hit |
| return nil, false |
| } |
| |
| // Add adds a value to the cache. |
| func (c *ARCCache) Add(key, value interface{}) { |
| c.lock.Lock() |
| defer c.lock.Unlock() |
| |
| // Check if the value is contained in T1 (recent), and potentially |
| // promote it to frequent T2 |
| if c.t1.Contains(key) { |
| c.t1.RemoveWithoutEvict(key) |
| c.t2.Add(key, value) |
| return |
| } |
| |
| // Check if the value is already in T2 (frequent) and update it |
| if c.t2.Contains(key) { |
| c.t2.Add(key, value) |
| return |
| } |
| |
| // Check if this value was recently evicted as part of the |
| // recently used list |
| if c.b1.Contains(key) { |
| // T1 set is too small, increase P appropriately |
| delta := 1 |
| b1Len := c.b1.Len() |
| b2Len := c.b2.Len() |
| if b2Len > b1Len { |
| delta = b2Len / b1Len |
| } |
| if c.p+delta >= c.size { |
| c.p = c.size |
| } else { |
| c.p += delta |
| } |
| |
| // Potentially need to make room in the cache |
| if c.t1.Len()+c.t2.Len() >= c.size { |
| c.replace(false) |
| } |
| |
| // Remove from B1 |
| c.b1.Remove(key) |
| |
| // Add the key to the frequently used list |
| c.t2.Add(key, value) |
| return |
| } |
| |
| // Check if this value was recently evicted as part of the |
| // frequently used list |
| if c.b2.Contains(key) { |
| // T2 set is too small, decrease P appropriately |
| delta := 1 |
| b1Len := c.b1.Len() |
| b2Len := c.b2.Len() |
| if b1Len > b2Len { |
| delta = b1Len / b2Len |
| } |
| if delta >= c.p { |
| c.p = 0 |
| } else { |
| c.p -= delta |
| } |
| |
| // Potentially need to make room in the cache |
| if c.t1.Len()+c.t2.Len() >= c.size { |
| c.replace(true) |
| } |
| |
| // Remove from B2 |
| c.b2.Remove(key) |
| |
| // Add the key to the frequently used list |
| c.t2.Add(key, value) |
| return |
| } |
| |
| // Potentially need to make room in the cache |
| if c.t1.Len()+c.t2.Len() >= c.size { |
| c.replace(false) |
| } |
| |
| // Keep the size of the ghost buffers trim |
| if c.b1.Len() > c.size-c.p { |
| c.b1.RemoveOldest() |
| } |
| if c.b2.Len() > c.p { |
| c.b2.RemoveOldest() |
| } |
| |
| // Add to the recently seen list |
| c.t1.Add(key, value) |
| return |
| } |
| |
| // replace is used to adaptively evict from either T1 or T2 |
| // based on the current learned value of P |
| func (c *ARCCache) replace(b2ContainsKey bool) { |
| t1Len := c.t1.Len() |
| if t1Len > 0 && (t1Len > c.p || (t1Len == c.p && b2ContainsKey)) { |
| k, _, ok := c.t1.RemoveOldest() |
| if ok { |
| c.b1.Add(k, nil) |
| } |
| } else { |
| k, _, ok := c.t2.RemoveOldest() |
| if ok { |
| c.b2.Add(k, nil) |
| } |
| } |
| } |
| |
| // Len returns the number of cached entries |
| func (c *ARCCache) Len() int { |
| c.lock.RLock() |
| defer c.lock.RUnlock() |
| return c.t1.Len() + c.t2.Len() |
| } |
| |
| // Keys returns all the cached keys |
| func (c *ARCCache) Keys() []interface{} { |
| c.lock.RLock() |
| defer c.lock.RUnlock() |
| k1 := c.t1.Keys() |
| k2 := c.t2.Keys() |
| return append(k1, k2...) |
| } |
| |
| func (c *ARCCache) subcaches() [4]simplelru.LRUCache { |
| return [4]simplelru.LRUCache{c.t1, c.t2, c.b1, c.b2} |
| } |
| |
| // Remove is used to purge a key from the cache |
| func (c *ARCCache) Remove(key interface{}) { |
| c.lock.Lock() |
| defer c.lock.Unlock() |
| for _, cache := range c.subcaches() { |
| if cache.Remove(key) { |
| return |
| } |
| } |
| } |
| |
| // Purge is used to clear the cache |
| func (c *ARCCache) Purge() { |
| c.lock.Lock() |
| defer c.lock.Unlock() |
| for _, cache := range c.subcaches() { |
| cache.Purge() |
| } |
| } |
| |
| // Contains is used to check if the cache contains a key |
| // without updating recency or frequency. |
| func (c *ARCCache) Contains(key interface{}) bool { |
| c.lock.RLock() |
| defer c.lock.RUnlock() |
| return c.t1.Contains(key) || c.t2.Contains(key) |
| } |
| |
| // Peek is used to inspect the cache value of a key |
| // without updating recency or frequency. |
| func (c *ARCCache) Peek(key interface{}) (value interface{}, ok bool) { |
| c.lock.RLock() |
| defer c.lock.RUnlock() |
| if val, ok := c.t1.Peek(key); ok { |
| return val, ok |
| } |
| return c.t2.Peek(key) |
| } |