From d4418943be0e1077dbd69923873d50b64874aa82 Mon Sep 17 00:00:00 2001 From: Yigong Liu Date: Sun, 22 Nov 2020 00:20:05 -0800 Subject: [PATCH 01/10] support simple expiring cache --- 2q.go | 66 ++-- arc.go | 94 ++++-- doc.go | 3 + expiringlru.go | 356 ++++++++++++++++++++ expiringlru_test.go | 671 +++++++++++++++++++++++++++++++++++++ lru.go | 33 +- rwlocker.go | 24 ++ simplelru/lru.go | 14 +- simplelru/lru_interface.go | 8 +- 9 files changed, 1196 insertions(+), 73 deletions(-) create mode 100644 expiringlru.go create mode 100644 expiringlru_test.go create mode 100644 rwlocker.go diff --git a/2q.go b/2q.go index 15fcad0..72e7d18 100644 --- a/2q.go +++ b/2q.go @@ -33,18 +33,21 @@ type TwoQueueCache struct { recent simplelru.LRUCache frequent simplelru.LRUCache recentEvict simplelru.LRUCache - lock sync.RWMutex + lock RWLocker } +// Option2Q define option to customize TwoQueueCache +type Option2Q func(c *TwoQueueCache) error + // New2Q creates a new TwoQueueCache using the default // values for the parameters. -func New2Q(size int) (*TwoQueueCache, error) { - return New2QParams(size, Default2QRecentRatio, Default2QGhostEntries) +func New2Q(size int, opts ...Option2Q) (*TwoQueueCache, error) { + return New2QParams(size, Default2QRecentRatio, Default2QGhostEntries, opts...) } // New2QParams creates a new TwoQueueCache using the provided // parameter values. -func New2QParams(size int, recentRatio, ghostRatio float64) (*TwoQueueCache, error) { +func New2QParams(size int, recentRatio, ghostRatio float64, opts ...Option2Q) (*TwoQueueCache, error) { if size <= 0 { return nil, fmt.Errorf("invalid size") } @@ -80,10 +83,23 @@ func New2QParams(size int, recentRatio, ghostRatio float64) (*TwoQueueCache, err recent: recent, frequent: frequent, recentEvict: recentEvict, + lock: &sync.RWMutex{}, + } + //apply options for customization + for _, opt := range opts { + if err = opt(c); err != nil { + return nil, err + } } return c, nil } +// NoLock2Q disables locking for TwoQueueCache +func NoLock2Q(c *TwoQueueCache) error { + c.lock = NoOpRWLocker{} + return nil +} + // Get looks up a key's value from the cache. func (c *TwoQueueCache) Get(key interface{}) (value interface{}, ok bool) { c.lock.Lock() @@ -106,8 +122,8 @@ func (c *TwoQueueCache) Get(key interface{}) (value interface{}, ok bool) { return nil, false } -// Add adds a value to the cache. -func (c *TwoQueueCache) Add(key, value interface{}) { +// Add adds a value to the cache, return evicted key/val if eviction happens. +func (c *TwoQueueCache) Add(key, value interface{}, evictedKeyVal ...*interface{}) (evicted bool) { c.lock.Lock() defer c.lock.Unlock() @@ -126,22 +142,29 @@ func (c *TwoQueueCache) Add(key, value interface{}) { return } + var evictedKey, evictedValue interface{} // If the value was recently evicted, add it to the // frequently used list if c.recentEvict.Contains(key) { - c.ensureSpace(true) + evictedKey, evictedValue, evicted = c.ensureSpace(true) c.recentEvict.Remove(key) c.frequent.Add(key, value) - return + } else { + // Add to the recently seen list + evictedKey, evictedValue, evicted = c.ensureSpace(false) + c.recent.Add(key, value) } - - // Add to the recently seen list - c.ensureSpace(false) - c.recent.Add(key, value) + if evicted && len(evictedKeyVal) > 0 { + *evictedKeyVal[0] = evictedKey + } + if evicted && len(evictedKeyVal) > 1 { + *evictedKeyVal[1] = evictedValue + } + return } // ensureSpace is used to ensure we have space in the cache -func (c *TwoQueueCache) ensureSpace(recentEvict bool) { +func (c *TwoQueueCache) ensureSpace(recentEvict bool) (key, value interface{}, evicted bool) { // If we have space, nothing to do recentLen := c.recent.Len() freqLen := c.frequent.Len() @@ -152,13 +175,13 @@ func (c *TwoQueueCache) ensureSpace(recentEvict bool) { // If the recent buffer is larger than // the target, evict from there if recentLen > 0 && (recentLen > c.recentSize || (recentLen == c.recentSize && !recentEvict)) { - k, _, _ := c.recent.RemoveOldest() - c.recentEvict.Add(k, nil) + key, value, evicted = c.recent.RemoveOldest() + c.recentEvict.Add(key, nil) return } // Remove from the frequent list otherwise - c.frequent.RemoveOldest() + return c.frequent.RemoveOldest() } // Len returns the number of items in the cache. @@ -179,18 +202,17 @@ func (c *TwoQueueCache) Keys() []interface{} { } // Remove removes the provided key from the cache. -func (c *TwoQueueCache) Remove(key interface{}) { +func (c *TwoQueueCache) Remove(key interface{}) bool { c.lock.Lock() defer c.lock.Unlock() if c.frequent.Remove(key) { - return + return true } if c.recent.Remove(key) { - return - } - if c.recentEvict.Remove(key) { - return + return true } + c.recentEvict.Remove(key) + return false } // Purge is used to completely clear the cache. diff --git a/arc.go b/arc.go index e396f84..df15bdc 100644 --- a/arc.go +++ b/arc.go @@ -24,11 +24,14 @@ type ARCCache struct { t2 simplelru.LRUCache // T2 is the LRU for frequently accessed items b2 simplelru.LRUCache // B2 is the LRU for evictions from t2 - lock sync.RWMutex + lock RWLocker } +// OptionARC defines option to customize ARCCache +type OptionARC func(*ARCCache) error + // NewARC creates an ARC of the given size -func NewARC(size int) (*ARCCache, error) { +func NewARC(size int, opts ...OptionARC) (*ARCCache, error) { // Create the sub LRUs b1, err := simplelru.NewLRU(size, nil) if err != nil { @@ -55,10 +58,23 @@ func NewARC(size int) (*ARCCache, error) { b1: b1, t2: t2, b2: b2, + lock: &sync.RWMutex{}, + } + //apply option settings + for _, opt := range opts { + if err = opt(c); err != nil { + return nil, err + } } return c, nil } +// NoLockARC disables locking for ARCCache +func NoLockARC(c *ARCCache) error { + c.lock = NoOpRWLocker{} + return nil +} + // Get looks up a key's value from the cache. func (c *ARCCache) Get(key interface{}) (value interface{}, ok bool) { c.lock.Lock() @@ -81,8 +97,8 @@ func (c *ARCCache) Get(key interface{}) (value interface{}, ok bool) { return nil, false } -// Add adds a value to the cache. -func (c *ARCCache) Add(key, value interface{}) { +// Add adds a value to the cache, return evicted key/val if it happens. +func (c *ARCCache) Add(key, value interface{}, evictedKeyVal ...*interface{}) (evicted bool) { c.lock.Lock() defer c.lock.Unlock() @@ -100,9 +116,10 @@ func (c *ARCCache) Add(key, value interface{}) { return } - // Check if this value was recently evicted as part of the - // recently used list + var evictedKey, evictedValue interface{} if c.b1.Contains(key) { + // Check if this value was recently evicted as part of the + // recently used list // T1 set is too small, increase P appropriately delta := 1 b1Len := c.b1.Len() @@ -118,7 +135,7 @@ func (c *ARCCache) Add(key, value interface{}) { // Potentially need to make room in the cache if c.t1.Len()+c.t2.Len() >= c.size { - c.replace(false) + evictedKey, evictedValue, evicted = c.replace(false) } // Remove from B1 @@ -126,12 +143,10 @@ func (c *ARCCache) Add(key, value interface{}) { // 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) { + } else if c.b2.Contains(key) { + // Check if this value was recently evicted as part of the + // frequently used list // T2 set is too small, decrease P appropriately delta := 1 b1Len := c.b1.Len() @@ -147,7 +162,7 @@ func (c *ARCCache) Add(key, value interface{}) { // Potentially need to make room in the cache if c.t1.Len()+c.t2.Len() >= c.size { - c.replace(true) + evictedKey, evictedValue, evicted = c.replace(true) } // Remove from B2 @@ -155,41 +170,49 @@ func (c *ARCCache) Add(key, value interface{}) { // Add the key to the frequently used list c.t2.Add(key, value) - return - } + } else { + // Brand new entry + // Potentially need to make room in the cache + if c.t1.Len()+c.t2.Len() >= c.size { + evictedKey, evictedValue, evicted = c.replace(false) + } - // 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() + } - // Keep the size of the ghost buffers trim - if c.b1.Len() > c.size-c.p { - c.b1.RemoveOldest() + // Add to the recently seen list + c.t1.Add(key, value) } - if c.b2.Len() > c.p { - c.b2.RemoveOldest() + if evicted && len(evictedKeyVal) > 0 { + *evictedKeyVal[0] = evictedKey } - - // Add to the recently seen list - c.t1.Add(key, value) + if evicted && len(evictedKeyVal) > 1 { + *evictedKeyVal[1] = evictedValue + } + 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) { +func (c *ARCCache) replace(b2ContainsKey bool) (k, v interface{}, ok bool) { t1Len := c.t1.Len() if t1Len > 0 && (t1Len > c.p || (t1Len == c.p && b2ContainsKey)) { - k, _, ok := c.t1.RemoveOldest() + k, v, ok = c.t1.RemoveOldest() if ok { c.b1.Add(k, nil) } } else { - k, _, ok := c.t2.RemoveOldest() + k, v, ok = c.t2.RemoveOldest() if ok { c.b2.Add(k, nil) } } + return } // Len returns the number of cached entries @@ -209,21 +232,22 @@ func (c *ARCCache) Keys() []interface{} { } // Remove is used to purge a key from the cache -func (c *ARCCache) Remove(key interface{}) { +func (c *ARCCache) Remove(key interface{}) bool { c.lock.Lock() defer c.lock.Unlock() if c.t1.Remove(key) { - return + return true } if c.t2.Remove(key) { - return + return true } if c.b1.Remove(key) { - return + return false } if c.b2.Remove(key) { - return + return false } + return false } // Purge is used to clear the cache diff --git a/doc.go b/doc.go index 2547df9..ebb824b 100644 --- a/doc.go +++ b/doc.go @@ -16,6 +16,9 @@ // ARC has been patented by IBM, so do not use it if that is problematic for // your program. // +// ExpiringCache wraps one of the above caches and make their entries expiring +// according to policies: ExpireAfterAccess or ExpireAfterWrite. +// // All caches in this package take locks while operating, and are therefore // thread-safe for consumers. package lru diff --git a/expiringlru.go b/expiringlru.go new file mode 100644 index 0000000..30ac7de --- /dev/null +++ b/expiringlru.go @@ -0,0 +1,356 @@ +package lru + +import ( + "container/list" + "fmt" + "sync" + "time" +) + +//common interface shared by 2q, arc and simple LRU, used as interface of backing LRU +type lruCache interface { + // Adds a value to the cache, returns evicted if happened and + // updates the "recently used"-ness of the key. + Add(k, v interface{}, evictedKeyVal ...*interface{}) (evicted bool) + // Returns key's value from the cache if found and + // updates the "recently used"-ness of the key. + Get(k interface{}) (v interface{}, ok bool) + // Removes a key from the cache + Remove(k interface{}) bool + // Returns key's value without updating the "recently used"-ness of the key. + Peek(key interface{}) (value interface{}, ok bool) + // Checks if a key exists in cache without updating the recent-ness. + Contains(k interface{}) bool + // Returns a slice of the keys in the cache, from oldest to newest. + Keys() []interface{} + // Returns the number of items in the cache. + Len() int + // Clears all cache entries. + Purge() +} + +type entry struct { + key interface{} + val interface{} + expirationTime time.Time + elem *list.Element +} + +func (e entry) String() string { + return fmt.Sprintf("%v,%v %v", e.key, e.val, e.expirationTime) +} + +//two expiration policies +type expiringType byte + +const ( + expireAfterWrite expiringType = iota + expireAfterAccess +) + +// ExpiringCache will wrap an existing LRU and make its entries expiring +// according to two policies: +// expireAfterAccess and expireAfterWrite (default) +// Internally keep a expireList sorted by entries' expirationTime +type ExpiringCache struct { + lru lruCache + expiration time.Duration + expireList *expireList + expireType expiringType + //placeholder for time.Now() for easier testing setup + timeNow func() time.Time + lock RWLocker +} + +// OptionExp defines option to customize ExpiringCache +type OptionExp func(c *ExpiringCache) error + +// NewExpiring2Q creates an expiring cache with specifized +// size and entries lifetime duration, backed by a 2-queue LRU +func NewExpiring2Q(size int, expir time.Duration, opts ...OptionExp) (elru *ExpiringCache, err error) { + //create a non synced LRU as backing store + lru, err := New2Q(size, NoLock2Q) + if err != nil { + return + } + elru, err = Expiring(expir, lru, opts...) + return +} + +// NewExpiringARC creates an expiring cache with specifized +// size and entries lifetime duration, backed by a ARC LRU +func NewExpiringARC(size int, expir time.Duration, opts ...OptionExp) (elru *ExpiringCache, err error) { + //create a non synced LRU as backing store + lru, err := NewARC(size, NoLockARC) + if err != nil { + return + } + elru, err = Expiring(expir, lru, opts...) + return +} + +// NewExpiringLRU creates an expiring cache with specifized +// size and entries lifetime duration, backed by a simple LRU +func NewExpiringLRU(size int, expir time.Duration, opts ...OptionExp) (elru *ExpiringCache, err error) { + //create a non synced LRU as backing store + lru, err := New(size, NoLock) + if err != nil { + return + } + elru, err = Expiring(expir, lru, opts...) + return +} + +// Expiring will wrap an existing LRU to make its entries +// expiring with specified duration +func Expiring(expir time.Duration, lru lruCache, opts ...OptionExp) (*ExpiringCache, error) { + //create expiring cache with default settings + elru := &ExpiringCache{ + lru: lru, + expiration: expir, + expireList: newExpireList(), + expireType: expireAfterWrite, + timeNow: time.Now, + lock: &sync.RWMutex{}, + } + //apply options to customize + for _, opt := range opts { + if err := opt(elru); err != nil { + return nil, err + } + } + return elru, nil +} + +// NoLockExp disables locking for ExpiringCache +func NoLockExp(elru *ExpiringCache) error { + elru.lock = NoOpRWLocker{} + return nil +} + +// ExpireAfterWrite sets expiring policy +func ExpireAfterWrite(elru *ExpiringCache) error { + elru.expireType = expireAfterWrite + return nil +} + +// ExpireAfterAccess sets expiring policy +func ExpireAfterAccess(elru *ExpiringCache) error { + elru.expireType = expireAfterAccess + return nil +} + +// TimeTicker sets the function used to return current time, for test setup +func TimeTicker(tn func() time.Time) OptionExp { + return func(elru *ExpiringCache) error { + elru.timeNow = tn + return nil + } +} + +// Add add a key/val pair to cache with cache's default expiration duration +// return evicted key/val pair if eviction happens. +// Should be used in most cases for better performance +func (elru *ExpiringCache) Add(k, v interface{}, evictedKeyVal ...*interface{}) (evicted bool) { + return elru.AddWithTTL(k, v, elru.expiration, evictedKeyVal...) +} + +// AddWithTTL add a key/val pair to cache with provided expiration duration +// return evicted key/val pair if eviction happens. +// Using this with variant expiration durations could cause degraded performance +func (elru *ExpiringCache) AddWithTTL(k, v interface{}, expiration time.Duration, evictedKeyVal ...*interface{}) (evicted bool) { + elru.lock.Lock() + defer elru.lock.Unlock() + now := elru.timeNow() + var ent *entry + var expired []*entry + if ent0, _ := elru.lru.Peek(k); ent0 != nil { + //update existing cache entry + ent = ent0.(*entry) + ent.val = v + ent.expirationTime = now.Add(expiration) + elru.expireList.MoveToFront(ent) + } else { + //first remove 1 possible expiration to add space for new entry + expired = elru.removeExpired(now, false) + //add new entry to expiration list + ent = &entry{ + key: k, + val: v, + expirationTime: now.Add(expiration), + } + elru.expireList.PushFront(ent) + } + // Add/Update cache entry in backing cache + var evictedKey, evictedVal interface{} + evicted = elru.lru.Add(k, ent, &evictedKey, &evictedVal) + //remove evicted ent from expireList + if evicted { + ent = evictedVal.(*entry) + evictedVal = ent.val + elru.expireList.Remove(ent) + } else if len(expired) > 0 { + evictedKey = expired[0].key + evictedVal = expired[0].val + evicted = true + } + if evicted && len(evictedKeyVal) > 0 { + *evictedKeyVal[0] = evictedKey + } + if evicted && len(evictedKeyVal) > 1 { + *evictedKeyVal[1] = evictedVal + } + return +} + +// Get returns key's value from the cache if found +func (elru *ExpiringCache) Get(k interface{}) (v interface{}, ok bool) { + elru.lock.Lock() + defer elru.lock.Unlock() + now := elru.timeNow() + if ent0, ok := elru.lru.Get(k); ok { + ent := ent0.(*entry) + if ent.expirationTime.After(now) { + if elru.expireType == expireAfterAccess { + ent.expirationTime = now.Add(elru.expiration) + elru.expireList.MoveToFront(ent) + } + return ent.val, true + } + } + return +} + +// Remove removes a key from the cache +func (elru *ExpiringCache) Remove(k interface{}) bool { + elru.lock.Lock() + defer elru.lock.Unlock() + if ent, _ := elru.lru.Peek(k); ent != nil { + elru.expireList.Remove(ent.(*entry)) + return elru.lru.Remove(k) + } + return false +} + +// Peek return key's value without updating the "recently used"-ness of the key. +// returns ok=false if k not found or entry expired +func (elru *ExpiringCache) Peek(k interface{}) (v interface{}, ok bool) { + elru.lock.RLock() + defer elru.lock.RUnlock() + if ent0, ok := elru.lru.Peek(k); ok { + ent := ent0.(*entry) + if ent.expirationTime.After(elru.timeNow()) { + return ent.val, true + } + return ent.val, false + } + return +} + +// Contains is used to check if the cache contains a key +// without updating recency or frequency. +func (elru *ExpiringCache) Contains(k interface{}) bool { + _, ok := elru.Peek(k) + return ok +} + +// Keys returns a slice of the keys in the cache. +// The frequently used keys are first in the returned slice. +func (elru *ExpiringCache) Keys() []interface{} { + elru.lock.Lock() + defer elru.lock.Unlock() + //to get accurate key set, remove all expired + elru.removeExpired(elru.timeNow(), true) + return elru.lru.Keys() +} + +// Len returns the number of items in the cache. +func (elru *ExpiringCache) Len() int { + elru.lock.Lock() + defer elru.lock.Unlock() + //to get accurate size, remove all expired + elru.removeExpired(elru.timeNow(), true) + return elru.lru.Len() +} + +// Purge is used to completely clear the cache. +func (elru *ExpiringCache) Purge() { + elru.lock.Lock() + defer elru.lock.Unlock() + elru.expireList.Init() + elru.lru.Purge() +} + +//either remove one (the oldest expired), or all expired +func (elru *ExpiringCache) removeExpired(now time.Time, removeAllExpired bool) (res []*entry) { + res = elru.expireList.RemoveExpired(now, removeAllExpired) + for i := 0; i < len(res); i++ { + elru.lru.Remove(res[i].key) + } + return +} + +// RemoveAllExpired remove all expired entries, can be called by cleanup goroutine +func (elru *ExpiringCache) RemoveAllExpired() { + elru.removeExpired(elru.timeNow(), true) +} + +// oldest entries are at front of expire list +type expireList struct { + expList *list.List +} + +func newExpireList() *expireList { + return &expireList{ + expList: list.New(), + } +} + +func (el *expireList) Init() { + el.expList.Init() +} + +func (el *expireList) PushFront(ent *entry) { + //When all operations use ExpiringCache default expiration, + //PushFront should succeed at first/front entry of list + for e := el.expList.Front(); e != nil; e = e.Next() { + if !ent.expirationTime.Before(e.Value.(*entry).expirationTime) { + ent.elem = el.expList.InsertBefore(ent, e) + return + } + } + ent.elem = el.expList.PushBack(ent) +} + +func (el *expireList) MoveToFront(ent *entry) { + //When all operations use ExpiringCache default expiration, + //MoveToFront should succeed at first/front entry of list + for e := el.expList.Front(); e != nil; e = e.Next() { + if !ent.expirationTime.Before(e.Value.(*entry).expirationTime) { + el.expList.MoveBefore(ent.elem, e) + return + } + } + el.expList.MoveAfter(ent.elem, el.expList.Back()) +} + +func (el *expireList) Remove(ent *entry) interface{} { + return el.expList.Remove(ent.elem) +} + +//either remove one (the oldest expired), or remove all expired +func (el *expireList) RemoveExpired(now time.Time, removeAllExpired bool) (res []*entry) { + for { + back := el.expList.Back() + if back == nil || back.Value.(*entry).expirationTime.After(now) { + break + } + //expired + ent := el.expList.Remove(back).(*entry) + res = append(res, ent) + if !removeAllExpired { + break + } + } + return +} diff --git a/expiringlru_test.go b/expiringlru_test.go new file mode 100644 index 0000000..2d0ce57 --- /dev/null +++ b/expiringlru_test.go @@ -0,0 +1,671 @@ +package lru + +import ( + "math/rand" + "sort" + "testing" + "time" +) + +func BenchmarkExpiring2Q_Rand(b *testing.B) { + l, err := NewExpiring2Q(8192, 5*time.Minute) + if err != nil { + b.Fatalf("err: %v", err) + } + + trace := make([]int64, b.N*2) + for i := 0; i < b.N*2; i++ { + trace[i] = rand.Int63() % 32768 + } + + b.ResetTimer() + + var hit, miss int + for i := 0; i < 2*b.N; i++ { + if i%2 == 0 { + l.Add(trace[i], trace[i]) + } else { + _, ok := l.Get(trace[i]) + if ok { + hit++ + } else { + miss++ + } + } + } + b.Logf("hit: %d miss: %d ratio: %f", hit, miss, float64(hit)/float64(miss)) +} + +func BenchmarkExpiring2Q_Freq(b *testing.B) { + l, err := NewExpiring2Q(8192, 5*time.Minute) + if err != nil { + b.Fatalf("err: %v", err) + } + + trace := make([]int64, b.N*2) + for i := 0; i < b.N*2; i++ { + if i%2 == 0 { + trace[i] = rand.Int63() % 16384 + } else { + trace[i] = rand.Int63() % 32768 + } + } + + b.ResetTimer() + + for i := 0; i < b.N; i++ { + l.Add(trace[i], trace[i]) + } + var hit, miss int + for i := 0; i < b.N; i++ { + _, ok := l.Get(trace[i]) + if ok { + hit++ + } else { + miss++ + } + } + b.Logf("hit: %d miss: %d ratio: %f", hit, miss, float64(hit)/float64(miss)) +} + +func BenchmarkExpiringARC_Rand(b *testing.B) { + l, err := NewExpiringARC(8192, 5*time.Minute) + if err != nil { + b.Fatalf("err: %v", err) + } + + trace := make([]int64, b.N*2) + for i := 0; i < b.N*2; i++ { + trace[i] = rand.Int63() % 32768 + } + + b.ResetTimer() + + var hit, miss int + for i := 0; i < 2*b.N; i++ { + if i%2 == 0 { + l.Add(trace[i], trace[i]) + } else { + _, ok := l.Get(trace[i]) + if ok { + hit++ + } else { + miss++ + } + } + } + b.Logf("hit: %d miss: %d ratio: %f", hit, miss, float64(hit)/float64(miss)) +} + +func BenchmarkExpiringARC_Freq(b *testing.B) { + l, err := NewExpiringARC(8192, 5*time.Minute) + if err != nil { + b.Fatalf("err: %v", err) + } + + trace := make([]int64, b.N*2) + for i := 0; i < b.N*2; i++ { + if i%2 == 0 { + trace[i] = rand.Int63() % 16384 + } else { + trace[i] = rand.Int63() % 32768 + } + } + + b.ResetTimer() + + for i := 0; i < b.N; i++ { + l.Add(trace[i], trace[i]) + } + var hit, miss int + for i := 0; i < b.N; i++ { + _, ok := l.Get(trace[i]) + if ok { + hit++ + } else { + miss++ + } + } + b.Logf("hit: %d miss: %d ratio: %f", hit, miss, float64(hit)/float64(miss)) +} + +func BenchmarkExpiringLRU_Rand(b *testing.B) { + l, err := NewExpiringLRU(8192, 5*time.Minute) + if err != nil { + b.Fatalf("err: %v", err) + } + + trace := make([]int64, b.N*2) + for i := 0; i < b.N*2; i++ { + trace[i] = rand.Int63() % 32768 + } + + b.ResetTimer() + + var hit, miss int + for i := 0; i < 2*b.N; i++ { + if i%2 == 0 { + l.Add(trace[i], trace[i]) + } else { + _, ok := l.Get(trace[i]) + if ok { + hit++ + } else { + miss++ + } + } + } + b.Logf("hit: %d miss: %d ratio: %f", hit, miss, float64(hit)/float64(miss)) +} + +func BenchmarkExpiringLRU_Freq(b *testing.B) { + l, err := NewExpiringLRU(8192, 5*time.Minute) + if err != nil { + b.Fatalf("err: %v", err) + } + + trace := make([]int64, b.N*2) + for i := 0; i < b.N*2; i++ { + if i%2 == 0 { + trace[i] = rand.Int63() % 16384 + } else { + trace[i] = rand.Int63() % 32768 + } + } + + b.ResetTimer() + + for i := 0; i < b.N; i++ { + l.Add(trace[i], trace[i]) + } + var hit, miss int + for i := 0; i < b.N; i++ { + _, ok := l.Get(trace[i]) + if ok { + hit++ + } else { + miss++ + } + } + b.Logf("hit: %d miss: %d ratio: %f", hit, miss, float64(hit)/float64(miss)) +} + +func TestExpiring2Q_RandomOps(t *testing.T) { + size := 128 + l, err := NewExpiring2Q(size, 5*time.Minute) + if err != nil { + t.Fatalf("err: %v", err) + } + + n := 200000 + for i := 0; i < n; i++ { + key := rand.Int63() % 512 + r := rand.Int63() + switch r % 3 { + case 0: + l.Add(key, key) + case 1: + l.Get(key) + case 2: + l.Remove(key) + } + + if l.Len() > size { + t.Fatalf("bad ExpiringCache size: %d, expected: %d", + l.Len(), size) + } + } +} + +func TestExpiringARC_RandomOps(t *testing.T) { + size := 128 + l, err := NewExpiringARC(size, 5*time.Minute) + if err != nil { + t.Fatalf("err: %v", err) + } + + n := 200000 + for i := 0; i < n; i++ { + key := rand.Int63() % 512 + r := rand.Int63() + switch r % 3 { + case 0: + l.Add(key, key) + case 1: + l.Get(key) + case 2: + l.Remove(key) + } + + if l.Len() > size { + t.Fatalf("bad ExpiringCache size: %d, expected: %d", + l.Len(), size) + } + } +} + +func TestExpiringLRU_RandomOps(t *testing.T) { + size := 128 + l, err := NewExpiringLRU(size, 5*time.Minute) + if err != nil { + t.Fatalf("err: %v", err) + } + + n := 200000 + for i := 0; i < n; i++ { + key := rand.Int63() % 512 + r := rand.Int63() + switch r % 3 { + case 0: + l.Add(key, key) + case 1: + l.Get(key) + case 2: + l.Remove(key) + } + + if l.Len() > size { + t.Fatalf("bad ExpiringCache size: %d, expected: %d", + l.Len(), size) + } + } +} + +// Test eviction by least-recently-used (2-queue LRU suuport retaining frequently-used) +func TestExpiring2Q_EvictionByLRU(t *testing.T) { + elru, err := NewExpiring2Q(3, 30*time.Second) + if err != nil { + t.Fatalf("failed to create expiring LRU") + } + for i := 0; i < 2; i++ { + elru.Add(i, i) + } + elru.Add(2, 2) + //Get(0),Get(1) will move 0, 1 to freq-used list + //2 will remain in recent-used list + for i := 0; i < 2; i++ { + elru.Get(i) + } + //next add 3,4; verify 2, 3 will be evicted + var ek, ev interface{} + for i := 3; i < 5; i++ { + evicted := elru.Add(i, i, &ek, &ev) + k, v := ek.(int), ev.(int) + if !evicted || k != (i-1) || v != (i-1) { + t.Fatalf("(%v %v) should be evicted, but got (%v,%v)", i-1, i-1, k, v) + } + } + if elru.Len() != 3 { + t.Fatalf("Expiring LRU eviction failed, expected 3 entries left, but found %v", elru.Len()) + } + keys := elru.Keys() + //since 0, 1 are touched twice (write & read) so + //they are in frequently used list, they are kept + //and 2,3,4 only touched once (write), so they + //moved thru "recent" list, with 2,3 evicted + for i, v := range []int{0, 1, 4} { + if v != keys[i] { + t.Fatalf("Expiring LRU eviction failed, expected keys {0,1,4} left, but found %v", elru.Keys()) + } + } +} + +//testTimer used to simulate time-elapse for expiration tests +type testTimer struct { + t time.Time +} + +func newTestTimer() *testTimer { return &testTimer{time.Now()} } +func (tt *testTimer) Now() time.Time { return tt.t } +func (tt *testTimer) Advance(d time.Duration) { tt.t = tt.t.Add(d) } + +// Test eviction by ExpireAfterWrite +func TestExpiring2Q_ExpireAfterWrite(t *testing.T) { + //use test timer for expiration + tt := newTestTimer() + elru, err := NewExpiring2Q(3, 30*time.Second, TimeTicker(tt.Now)) + if err != nil { + t.Fatalf("failed to create expiring LRU") + } + for i := 0; i < 2; i++ { + elru.Add(i, i) + } + //test timer ticks 20 seconds + tt.Advance(20 * time.Second) + //add fresher entry <2,2> to cache + elru.Add(2, 2) + //Get(0),Get(1) will move 0, 1 to freq-used list + //2 will remain in recent-used list + for i := 0; i < 2; i++ { + elru.Get(i) + } + //test timer advance another 15 seconds, entries <0,0>,<1,1> timeout & expire now, + //so they should be evicted, although they are more recently retrieved than <2,2> + tt.Advance(15 * time.Second) + //next add 3,4; verify 0,1 will be evicted + var ek, ev interface{} + for i := 3; i < 5; i++ { + evicted := elru.Add(i, i, &ek, &ev) + k, v := ek.(int), ev.(int) + if !evicted || k != (i-3) || v != (i-3) { + t.Fatalf("(%v %v) should be evicted, but got (%v,%v)", i-3, i-3, k, v) + } + } + if elru.Len() != 3 { + t.Fatalf("Expiring LRU eviction failed, expected 3 entries left, but found %v", elru.Len()) + } + keys := elru.Keys() + sort.Slice(keys, func(i, j int) bool { return keys[i].(int) < keys[j].(int) }) + //althoug 0, 1 are touched twice (write & read) so + //they are in frequently used list, they are evicted because expiration + //and 2,3,4 will be kept + for i, v := range []int{2, 3, 4} { + if v != keys[i] { + t.Fatalf("Expiring LRU eviction failed, expected keys {2,3,4} left, but found %v", elru.Keys()) + } + } +} + +// Test eviction by ExpireAfterAccess: basically same access sequence as above case +// but different result because of ExpireAfterAccess +func TestExpiring2Q_ExpireAfterAccess(t *testing.T) { + //use test timer for expiration + tt := newTestTimer() + elru, err := NewExpiring2Q(3, 30*time.Second, TimeTicker(tt.Now), ExpireAfterAccess) + if err != nil { + t.Fatalf("failed to create expiring LRU") + } + for i := 0; i < 2; i++ { + elru.Add(i, i) + } + //test timer ticks 20 seconds + tt.Advance(20 * time.Second) + //add fresher entry <2,2> to cache + elru.Add(2, 2) + //Get(0),Get(1) will move 0, 1 to freq-used list + //also moved them to back in expire list with newer timestamp + //2 will remain in recent-used list + for i := 0; i < 2; i++ { + elru.Get(i) + } + //test timer advance another 15 seconds, none expired + //and 2 in recent list + tt.Advance(15 * time.Second) + //next add 3,4; verify 2,3 will be evicted, because 0,1 in freq list, not expired + for i := 3; i < 5; i++ { + elru.Add(i, i) + } + if elru.Len() != 3 { + t.Fatalf("Expiring LRU eviction failed, expected 3 entries left, but found %v", elru.Len()) + } + keys := elru.Keys() + sort.Slice(keys, func(i, j int) bool { return keys[i].(int) < keys[j].(int) }) + //and 0,1,4 will be kept + for i, v := range []int{0, 1, 4} { + if v != keys[i] { + t.Fatalf("Expiring LRU eviction failed, expected keys {0,1,4} left, but found %v", elru.Keys()) + } + } +} + +// Test eviction by ExpireAfterWrite +func TestExpiringARC_ExpireAfterWrite(t *testing.T) { + //use test timer for expiration + tt := newTestTimer() + elru, err := NewExpiringARC(3, 30*time.Second, TimeTicker(tt.Now)) + if err != nil { + t.Fatalf("failed to create expiring LRU") + } + for i := 0; i < 2; i++ { + elru.Add(i, i) + } + //test timer ticks 20 seconds + tt.Advance(20 * time.Second) + //add fresher entry <2,2> to cache + elru.Add(2, 2) + //Get(0),Get(1) will move 0, 1 to freq-used list + //2 will remain in recent-used list + for i := 0; i < 2; i++ { + elru.Get(i) + } + //test timer advance another 15 seconds, entries <0,0>,<1,1> timeout & expire now, + //so they should be evicted, although they are more recently retrieved than <2,2> + tt.Advance(15 * time.Second) + //next add 3,4; verify 0,1 will be evicted + var ek, ev interface{} + for i := 3; i < 5; i++ { + evicted := elru.Add(i, i, &ek, &ev) + k, v := ek.(int), ev.(int) + if !evicted || k != (i-3) || v != (i-3) { + t.Fatalf("(%v %v) should be evicted, but got (%v,%v)", i-3, i-3, k, v) + } + } + if elru.Len() != 3 { + t.Fatalf("Expiring LRU eviction failed, expected 3 entries left, but found %v", elru.Len()) + } + keys := elru.Keys() + sort.Slice(keys, func(i, j int) bool { return keys[i].(int) < keys[j].(int) }) + //althoug 0, 1 are touched twice (write & read) so + //they are in frequently used list, they are evicted because expiration + //and 2,3,4 will be kept + for i, v := range []int{2, 3, 4} { + if v != keys[i] { + t.Fatalf("Expiring LRU eviction failed, expected keys {2,3,4} left, but found %v", elru.Keys()) + } + } +} + +// Test eviction by ExpireAfterAccess: basically same access sequence as above case +// but different result because of ExpireAfterAccess +func TestExpiringARC_ExpireAfterAccess(t *testing.T) { + //use test timer for expiration + tt := newTestTimer() + elru, err := NewExpiringARC(3, 30*time.Second, TimeTicker(tt.Now), ExpireAfterAccess) + if err != nil { + t.Fatalf("failed to create expiring LRU") + } + for i := 0; i < 2; i++ { + elru.Add(i, i) + } + //test timer ticks 20 seconds + tt.Advance(20 * time.Second) + //add fresher entry <2,2> to cache + elru.Add(2, 2) + //Get(0),Get(1) will move 0, 1 to freq-used list + //also moved them to back in expire list with newer timestamp + //2 will remain in recent-used list + for i := 0; i < 2; i++ { + elru.Get(i) + } + //test timer advance another 15 seconds, none expired + //and 2 in recent list + tt.Advance(15 * time.Second) + //next add 3,4; verify 2,3 will be evicted, because 0,1 in freq list, not expired + for i := 3; i < 5; i++ { + elru.Add(i, i) + } + if elru.Len() != 3 { + t.Fatalf("Expiring LRU eviction failed, expected 3 entries left, but found %v", elru.Len()) + } + keys := elru.Keys() + sort.Slice(keys, func(i, j int) bool { return keys[i].(int) < keys[j].(int) }) + //and 0,1,4 will be kept + for i, v := range []int{0, 1, 4} { + if v != keys[i] { + t.Fatalf("Expiring LRU eviction failed, expected keys {0,1,4} left, but found %v", elru.Keys()) + } + } +} + +// Test eviction by ExpireAfterWrite +func TestExpiringLRU_ExpireAfterWrite(t *testing.T) { + //use test timer for expiration + tt := newTestTimer() + elru, err := NewExpiringLRU(3, 30*time.Second, TimeTicker(tt.Now)) + if err != nil { + t.Fatalf("failed to create expiring LRU") + } + for i := 0; i < 2; i++ { + elru.Add(i, i) + } + //test timer ticks 20 seconds + tt.Advance(20 * time.Second) + //add fresher entry <2,2> to cache + elru.Add(2, 2) + //Get(0),Get(1) will move 0, 1 to freq-used list + //2 will remain in recent-used list + for i := 0; i < 2; i++ { + elru.Get(i) + } + //test timer advance another 15 seconds, entries <0,0>,<1,1> timeout & expire now, + //so they should be evicted, although they are more recently retrieved than <2,2> + tt.Advance(15 * time.Second) + //next add 3,4; verify 0,1 will be evicted + var ek, ev interface{} + for i := 3; i < 5; i++ { + evicted := elru.Add(i, i, &ek, &ev) + k, v := ek.(int), ev.(int) + if !evicted || k != (i-3) || v != (i-3) { + t.Fatalf("(%v %v) should be evicted, but got (%v,%v)", i-3, i-3, k, v) + } + } + if elru.Len() != 3 { + t.Fatalf("Expiring LRU eviction failed, expected 3 entries left, but found %v", elru.Len()) + } + keys := elru.Keys() + sort.Slice(keys, func(i, j int) bool { return keys[i].(int) < keys[j].(int) }) + //althoug 0, 1 are touched twice (write & read) so + //they are in frequently used list, they are evicted because expiration + //and 2,3,4 will be kept + for i, v := range []int{2, 3, 4} { + if v != keys[i] { + t.Fatalf("Expiring LRU eviction failed, expected keys {2,3,4} left, but found %v", elru.Keys()) + } + } +} + +// Test eviction by ExpireAfterAccess: basically same access sequence as above case +// but different result because of ExpireAfterAccess +func TestExpiringLRU_ExpireAfterAccess(t *testing.T) { + //use test timer for expiration + tt := newTestTimer() + elru, err := NewExpiringLRU(3, 30*time.Second, TimeTicker(tt.Now), ExpireAfterAccess) + if err != nil { + t.Fatalf("failed to create expiring LRU") + } + for i := 0; i < 2; i++ { + elru.Add(i, i) + } + //test timer ticks 20 seconds + tt.Advance(20 * time.Second) + //add fresher entry <2,2> to cache + elru.Add(2, 2) + //Get(0),Get(1) will move 0, 1 to back of access list + //also moved them to back in expire list with newer timestamp + //access list will be 2,0,1 + for i := 0; i < 2; i++ { + elru.Get(i) + } + //test timer advance another 15 seconds, none expired + tt.Advance(15 * time.Second) + //next add 3,4; verify 2,0 will be evicted + for i := 3; i < 5; i++ { + elru.Add(i, i) + } + if elru.Len() != 3 { + t.Fatalf("Expiring LRU eviction failed, expected 3 entries left, but found %v", elru.Len()) + } + keys := elru.Keys() + sort.Slice(keys, func(i, j int) bool { return keys[i].(int) < keys[j].(int) }) + //and 1,3,4 will be kept + for i, v := range []int{1, 3, 4} { + if v != keys[i] { + t.Fatalf("Expiring LRU eviction failed, expected keys {1,3,4} left, but found %v", elru.Keys()) + } + } +} + +func TestExpiring2Q(t *testing.T) { + l, err := NewExpiring2Q(128, 5*time.Minute) + if err != nil { + t.Fatalf("err: %v", err) + } + + for i := 0; i < 256; i++ { + l.Add(i, i) + } + if l.Len() != 128 { + t.Fatalf("bad len: %v", l.Len()) + } + + for i, k := range l.Keys() { + if v, ok := l.Get(k); !ok || v != k || v != i+128 { + t.Fatalf("bad key: %v", k) + } + } + for i := 0; i < 128; i++ { + _, ok := l.Get(i) + if ok { + t.Fatalf("should be evicted") + } + } + for i := 128; i < 256; i++ { + _, ok := l.Get(i) + if !ok { + t.Fatalf("should not be evicted") + } + } + for i := 128; i < 192; i++ { + l.Remove(i) + _, ok := l.Get(i) + if ok { + t.Fatalf("should be deleted") + } + } + + l.Purge() + if l.Len() != 0 { + t.Fatalf("bad len: %v", l.Len()) + } + if _, ok := l.Get(200); ok { + t.Fatalf("should contain nothing") + } +} + +// Test that Contains doesn't update recent-ness +func TestExpiring2Q_Contains(t *testing.T) { + l, err := NewExpiring2Q(2, 5*time.Minute) + if err != nil { + t.Fatalf("err: %v", err) + } + + l.Add(1, 1) + l.Add(2, 2) + if !l.Contains(1) { + t.Errorf("1 should be contained") + } + + l.Add(3, 3) + if l.Contains(1) { + t.Errorf("Contains should not have updated recent-ness of 1") + } +} + +// Test that Peek doesn't update recent-ness +func TestExpiring2Q_Peek(t *testing.T) { + l, err := NewExpiring2Q(2, 5*time.Minute) + if err != nil { + t.Fatalf("err: %v", err) + } + + l.Add(1, 1) + l.Add(2, 2) + if v, ok := l.Peek(1); !ok || v != 1 { + t.Errorf("1 should be set to 1: %v, %v", v, ok) + } + + l.Add(3, 3) + if l.Contains(1) { + t.Errorf("should not have updated recent-ness of 1") + } +} diff --git a/lru.go b/lru.go index aa52433..cd7f99b 100644 --- a/lru.go +++ b/lru.go @@ -9,27 +9,44 @@ import ( // Cache is a thread-safe fixed size LRU cache. type Cache struct { lru simplelru.LRUCache - lock sync.RWMutex + lock RWLocker } +// Option to customize LRUCache +type Option func(*Cache) error + // New creates an LRU of the given size. -func New(size int) (*Cache, error) { - return NewWithEvict(size, nil) +func New(size int, opts ...Option) (*Cache, error) { + return NewWithEvict(size, nil, opts...) } // NewWithEvict constructs a fixed size cache with the given eviction // callback. -func NewWithEvict(size int, onEvicted func(key interface{}, value interface{})) (*Cache, error) { +func NewWithEvict(size int, onEvicted func(key interface{}, value interface{}), opts ...Option) (*Cache, error) { + //create a cache with default settings lru, err := simplelru.NewLRU(size, simplelru.EvictCallback(onEvicted)) if err != nil { return nil, err } c := &Cache{ - lru: lru, + lru: lru, + lock: &sync.RWMutex{}, + } + //apply options for custimization + for _, opt := range opts { + if err = opt(c); err != nil { + return nil, err + } } return c, nil } +// NoLock disables locking for LRUCache +func NoLock(c *Cache) error { + c.lock = NoOpRWLocker{} + return nil +} + // Purge is used to completely clear the cache. func (c *Cache) Purge() { c.lock.Lock() @@ -37,10 +54,10 @@ func (c *Cache) Purge() { c.lock.Unlock() } -// Add adds a value to the cache. Returns true if an eviction occurred. -func (c *Cache) Add(key, value interface{}) (evicted bool) { +// Add adds a value to the cache. Returns true and evicted key/val if an eviction occurred. +func (c *Cache) Add(key, value interface{}, evictedKeyVal ...*interface{}) (evicted bool) { c.lock.Lock() - evicted = c.lru.Add(key, value) + evicted = c.lru.Add(key, value, evictedKeyVal...) c.lock.Unlock() return evicted } diff --git a/rwlocker.go b/rwlocker.go new file mode 100644 index 0000000..58012f8 --- /dev/null +++ b/rwlocker.go @@ -0,0 +1,24 @@ +package lru + +// RWLocker define base interface of sync.RWMutex +type RWLocker interface { + Lock() + Unlock() + RLock() + RUnlock() +} + +// NoOpRWLocker is a dummy noop implementation of RWLocker interface +type NoOpRWLocker struct{} + +// Lock perform noop Lock() operation +func (nop NoOpRWLocker) Lock() {} + +// Unlock perform noop Unlock() operation +func (nop NoOpRWLocker) Unlock() {} + +// RLock perform noop RLock() operation +func (nop NoOpRWLocker) RLock() {} + +// RUnlock perform noop RUnlock() operation +func (nop NoOpRWLocker) RUnlock() {} diff --git a/simplelru/lru.go b/simplelru/lru.go index 9233583..259e6b3 100644 --- a/simplelru/lru.go +++ b/simplelru/lru.go @@ -48,7 +48,7 @@ func (c *LRU) Purge() { } // Add adds a value to the cache. Returns true if an eviction occurred. -func (c *LRU) Add(key, value interface{}) (evicted bool) { +func (c *LRU) Add(key, value interface{}, evictedKeyVal ...*interface{}) (evict bool) { // Check for existing item if ent, ok := c.items[key]; ok { c.evictList.MoveToFront(ent) @@ -61,12 +61,18 @@ func (c *LRU) Add(key, value interface{}) (evicted bool) { entry := c.evictList.PushFront(ent) c.items[key] = entry - evict := c.evictList.Len() > c.size + evict = c.evictList.Len() > c.size // Verify size not exceeded if evict { - c.removeOldest() + k, v, _ := c.RemoveOldest() + if len(evictedKeyVal) > 0 { + *evictedKeyVal[0] = k + } + if len(evictedKeyVal) > 1 { + *evictedKeyVal[1] = v + } } - return evict + return } // Get looks up a key's value from the cache. diff --git a/simplelru/lru_interface.go b/simplelru/lru_interface.go index cb7f8ca..33000bd 100644 --- a/simplelru/lru_interface.go +++ b/simplelru/lru_interface.go @@ -3,9 +3,9 @@ package simplelru // LRUCache is the interface for simple LRU cache. type LRUCache interface { - // Adds a value to the cache, returns true if an eviction occurred and - // updates the "recently used"-ness of the key. - Add(key, value interface{}) bool + // Adds a value to the cache, returns true if an eviction occurred + // return evicted key/val and updates the "recently used"-ness of the key. + Add(key, value interface{}, evictedKeyVal ...*interface{}) bool // Returns key's value from the cache and // updates the "recently used"-ness of the key. #value, isFound @@ -36,5 +36,5 @@ type LRUCache interface { Purge() // Resizes cache, returning number evicted - Resize(int) int + Resize(size int) int } From a3c9413ccb51db5a9bdd89f7540f6e0dd8c21794 Mon Sep 17 00:00:00 2001 From: Yigong Liu Date: Sun, 22 Nov 2020 00:53:31 -0800 Subject: [PATCH 02/10] fix lint issues --- 2q.go | 4 +- arc.go | 11 ++-- expiringlru.go | 44 +++++++------- expiringlru_test.go | 140 ++++++++++++++++++++++---------------------- lru.go | 4 +- 5 files changed, 102 insertions(+), 101 deletions(-) diff --git a/2q.go b/2q.go index 72e7d18..6006ffe 100644 --- a/2q.go +++ b/2q.go @@ -85,7 +85,7 @@ func New2QParams(size int, recentRatio, ghostRatio float64, opts ...Option2Q) (* recentEvict: recentEvict, lock: &sync.RWMutex{}, } - //apply options for customization + // Apply options for customization for _, opt := range opts { if err = opt(c); err != nil { return nil, err @@ -160,7 +160,7 @@ func (c *TwoQueueCache) Add(key, value interface{}, evictedKeyVal ...*interface{ if evicted && len(evictedKeyVal) > 1 { *evictedKeyVal[1] = evictedValue } - return + return evicted } // ensureSpace is used to ensure we have space in the cache diff --git a/arc.go b/arc.go index df15bdc..0770b70 100644 --- a/arc.go +++ b/arc.go @@ -60,7 +60,7 @@ func NewARC(size int, opts ...OptionARC) (*ARCCache, error) { b2: b2, lock: &sync.RWMutex{}, } - //apply option settings + // Apply option settings for _, opt := range opts { if err = opt(c); err != nil { return nil, err @@ -117,7 +117,8 @@ func (c *ARCCache) Add(key, value interface{}, evictedKeyVal ...*interface{}) (e } var evictedKey, evictedValue interface{} - if c.b1.Contains(key) { + switch { + case c.b1.Contains(key): // Check if this value was recently evicted as part of the // recently used list // T1 set is too small, increase P appropriately @@ -144,7 +145,7 @@ func (c *ARCCache) Add(key, value interface{}, evictedKeyVal ...*interface{}) (e // Add the key to the frequently used list c.t2.Add(key, value) - } else if c.b2.Contains(key) { + case c.b2.Contains(key): // Check if this value was recently evicted as part of the // frequently used list // T2 set is too small, decrease P appropriately @@ -170,7 +171,7 @@ func (c *ARCCache) Add(key, value interface{}, evictedKeyVal ...*interface{}) (e // Add the key to the frequently used list c.t2.Add(key, value) - } else { + default: // Brand new entry // Potentially need to make room in the cache if c.t1.Len()+c.t2.Len() >= c.size { @@ -194,7 +195,7 @@ func (c *ARCCache) Add(key, value interface{}, evictedKeyVal ...*interface{}) (e if evicted && len(evictedKeyVal) > 1 { *evictedKeyVal[1] = evictedValue } - return + return evicted } // replace is used to adaptively evict from either T1 or T2 diff --git a/expiringlru.go b/expiringlru.go index 30ac7de..3708caf 100644 --- a/expiringlru.go +++ b/expiringlru.go @@ -7,7 +7,7 @@ import ( "time" ) -//common interface shared by 2q, arc and simple LRU, used as interface of backing LRU +// common interface shared by 2q, arc and simple LRU, used as interface of backing LRU type lruCache interface { // Adds a value to the cache, returns evicted if happened and // updates the "recently used"-ness of the key. @@ -40,7 +40,7 @@ func (e entry) String() string { return fmt.Sprintf("%v,%v %v", e.key, e.val, e.expirationTime) } -//two expiration policies +// two expiration policies type expiringType byte const ( @@ -57,7 +57,7 @@ type ExpiringCache struct { expiration time.Duration expireList *expireList expireType expiringType - //placeholder for time.Now() for easier testing setup + // placeholder for time.Now() for easier testing setup timeNow func() time.Time lock RWLocker } @@ -68,7 +68,7 @@ type OptionExp func(c *ExpiringCache) error // NewExpiring2Q creates an expiring cache with specifized // size and entries lifetime duration, backed by a 2-queue LRU func NewExpiring2Q(size int, expir time.Duration, opts ...OptionExp) (elru *ExpiringCache, err error) { - //create a non synced LRU as backing store + // create a non synced LRU as backing store lru, err := New2Q(size, NoLock2Q) if err != nil { return @@ -80,7 +80,7 @@ func NewExpiring2Q(size int, expir time.Duration, opts ...OptionExp) (elru *Expi // NewExpiringARC creates an expiring cache with specifized // size and entries lifetime duration, backed by a ARC LRU func NewExpiringARC(size int, expir time.Duration, opts ...OptionExp) (elru *ExpiringCache, err error) { - //create a non synced LRU as backing store + // create a non synced LRU as backing store lru, err := NewARC(size, NoLockARC) if err != nil { return @@ -92,7 +92,7 @@ func NewExpiringARC(size int, expir time.Duration, opts ...OptionExp) (elru *Exp // NewExpiringLRU creates an expiring cache with specifized // size and entries lifetime duration, backed by a simple LRU func NewExpiringLRU(size int, expir time.Duration, opts ...OptionExp) (elru *ExpiringCache, err error) { - //create a non synced LRU as backing store + // create a non synced LRU as backing store lru, err := New(size, NoLock) if err != nil { return @@ -104,7 +104,7 @@ func NewExpiringLRU(size int, expir time.Duration, opts ...OptionExp) (elru *Exp // Expiring will wrap an existing LRU to make its entries // expiring with specified duration func Expiring(expir time.Duration, lru lruCache, opts ...OptionExp) (*ExpiringCache, error) { - //create expiring cache with default settings + // create expiring cache with default settings elru := &ExpiringCache{ lru: lru, expiration: expir, @@ -113,7 +113,7 @@ func Expiring(expir time.Duration, lru lruCache, opts ...OptionExp) (*ExpiringCa timeNow: time.Now, lock: &sync.RWMutex{}, } - //apply options to customize + // apply options to customize for _, opt := range opts { if err := opt(elru); err != nil { return nil, err @@ -165,15 +165,15 @@ func (elru *ExpiringCache) AddWithTTL(k, v interface{}, expiration time.Duration var ent *entry var expired []*entry if ent0, _ := elru.lru.Peek(k); ent0 != nil { - //update existing cache entry + // update existing cache entry ent = ent0.(*entry) ent.val = v ent.expirationTime = now.Add(expiration) elru.expireList.MoveToFront(ent) } else { - //first remove 1 possible expiration to add space for new entry + // first remove 1 possible expiration to add space for new entry expired = elru.removeExpired(now, false) - //add new entry to expiration list + // add new entry to expiration list ent = &entry{ key: k, val: v, @@ -184,7 +184,7 @@ func (elru *ExpiringCache) AddWithTTL(k, v interface{}, expiration time.Duration // Add/Update cache entry in backing cache var evictedKey, evictedVal interface{} evicted = elru.lru.Add(k, ent, &evictedKey, &evictedVal) - //remove evicted ent from expireList + // remove evicted ent from expireList if evicted { ent = evictedVal.(*entry) evictedVal = ent.val @@ -200,7 +200,7 @@ func (elru *ExpiringCache) AddWithTTL(k, v interface{}, expiration time.Duration if evicted && len(evictedKeyVal) > 1 { *evictedKeyVal[1] = evictedVal } - return + return evicted } // Get returns key's value from the cache if found @@ -259,7 +259,7 @@ func (elru *ExpiringCache) Contains(k interface{}) bool { func (elru *ExpiringCache) Keys() []interface{} { elru.lock.Lock() defer elru.lock.Unlock() - //to get accurate key set, remove all expired + // to get accurate key set, remove all expired elru.removeExpired(elru.timeNow(), true) return elru.lru.Keys() } @@ -268,7 +268,7 @@ func (elru *ExpiringCache) Keys() []interface{} { func (elru *ExpiringCache) Len() int { elru.lock.Lock() defer elru.lock.Unlock() - //to get accurate size, remove all expired + // to get accurate size, remove all expired elru.removeExpired(elru.timeNow(), true) return elru.lru.Len() } @@ -281,7 +281,7 @@ func (elru *ExpiringCache) Purge() { elru.lru.Purge() } -//either remove one (the oldest expired), or all expired +// either remove one (the oldest expired), or all expired func (elru *ExpiringCache) removeExpired(now time.Time, removeAllExpired bool) (res []*entry) { res = elru.expireList.RemoveExpired(now, removeAllExpired) for i := 0; i < len(res); i++ { @@ -311,8 +311,8 @@ func (el *expireList) Init() { } func (el *expireList) PushFront(ent *entry) { - //When all operations use ExpiringCache default expiration, - //PushFront should succeed at first/front entry of list + // When all operations use ExpiringCache default expiration, + // PushFront should succeed at first/front entry of list for e := el.expList.Front(); e != nil; e = e.Next() { if !ent.expirationTime.Before(e.Value.(*entry).expirationTime) { ent.elem = el.expList.InsertBefore(ent, e) @@ -323,8 +323,8 @@ func (el *expireList) PushFront(ent *entry) { } func (el *expireList) MoveToFront(ent *entry) { - //When all operations use ExpiringCache default expiration, - //MoveToFront should succeed at first/front entry of list + // When all operations use ExpiringCache default expiration, + // MoveToFront should succeed at first/front entry of list for e := el.expList.Front(); e != nil; e = e.Next() { if !ent.expirationTime.Before(e.Value.(*entry).expirationTime) { el.expList.MoveBefore(ent.elem, e) @@ -338,14 +338,14 @@ func (el *expireList) Remove(ent *entry) interface{} { return el.expList.Remove(ent.elem) } -//either remove one (the oldest expired), or remove all expired +// either remove one (the oldest expired), or remove all expired func (el *expireList) RemoveExpired(now time.Time, removeAllExpired bool) (res []*entry) { for { back := el.expList.Back() if back == nil || back.Value.(*entry).expirationTime.After(now) { break } - //expired + // expired ent := el.expList.Remove(back).(*entry) res = append(res, ent) if !removeAllExpired { diff --git a/expiringlru_test.go b/expiringlru_test.go index 2d0ce57..2540f33 100644 --- a/expiringlru_test.go +++ b/expiringlru_test.go @@ -281,12 +281,12 @@ func TestExpiring2Q_EvictionByLRU(t *testing.T) { elru.Add(i, i) } elru.Add(2, 2) - //Get(0),Get(1) will move 0, 1 to freq-used list - //2 will remain in recent-used list + // Get(0),Get(1) will move 0, 1 to freq-used list + // 2 will remain in recent-used list for i := 0; i < 2; i++ { elru.Get(i) } - //next add 3,4; verify 2, 3 will be evicted + // next add 3,4; verify 2, 3 will be evicted var ek, ev interface{} for i := 3; i < 5; i++ { evicted := elru.Add(i, i, &ek, &ev) @@ -299,10 +299,10 @@ func TestExpiring2Q_EvictionByLRU(t *testing.T) { t.Fatalf("Expiring LRU eviction failed, expected 3 entries left, but found %v", elru.Len()) } keys := elru.Keys() - //since 0, 1 are touched twice (write & read) so - //they are in frequently used list, they are kept - //and 2,3,4 only touched once (write), so they - //moved thru "recent" list, with 2,3 evicted + // since 0, 1 are touched twice (write & read) so + // they are in frequently used list, they are kept + // and 2,3,4 only touched once (write), so they + // moved thru "recent" list, with 2,3 evicted for i, v := range []int{0, 1, 4} { if v != keys[i] { t.Fatalf("Expiring LRU eviction failed, expected keys {0,1,4} left, but found %v", elru.Keys()) @@ -310,7 +310,7 @@ func TestExpiring2Q_EvictionByLRU(t *testing.T) { } } -//testTimer used to simulate time-elapse for expiration tests +// testTimer used to simulate time-elapse for expiration tests type testTimer struct { t time.Time } @@ -321,7 +321,7 @@ func (tt *testTimer) Advance(d time.Duration) { tt.t = tt.t.Add(d) } // Test eviction by ExpireAfterWrite func TestExpiring2Q_ExpireAfterWrite(t *testing.T) { - //use test timer for expiration + // use test timer for expiration tt := newTestTimer() elru, err := NewExpiring2Q(3, 30*time.Second, TimeTicker(tt.Now)) if err != nil { @@ -330,19 +330,19 @@ func TestExpiring2Q_ExpireAfterWrite(t *testing.T) { for i := 0; i < 2; i++ { elru.Add(i, i) } - //test timer ticks 20 seconds + // test timer ticks 20 seconds tt.Advance(20 * time.Second) - //add fresher entry <2,2> to cache + // add fresher entry <2,2> to cache elru.Add(2, 2) - //Get(0),Get(1) will move 0, 1 to freq-used list - //2 will remain in recent-used list + // Get(0),Get(1) will move 0, 1 to freq-used list + // 2 will remain in recent-used list for i := 0; i < 2; i++ { elru.Get(i) } - //test timer advance another 15 seconds, entries <0,0>,<1,1> timeout & expire now, - //so they should be evicted, although they are more recently retrieved than <2,2> + // test timer advance another 15 seconds, entries <0,0>,<1,1> timeout & expire now, + // so they should be evicted, although they are more recently retrieved than <2,2> tt.Advance(15 * time.Second) - //next add 3,4; verify 0,1 will be evicted + // next add 3,4; verify 0,1 will be evicted var ek, ev interface{} for i := 3; i < 5; i++ { evicted := elru.Add(i, i, &ek, &ev) @@ -356,9 +356,9 @@ func TestExpiring2Q_ExpireAfterWrite(t *testing.T) { } keys := elru.Keys() sort.Slice(keys, func(i, j int) bool { return keys[i].(int) < keys[j].(int) }) - //althoug 0, 1 are touched twice (write & read) so - //they are in frequently used list, they are evicted because expiration - //and 2,3,4 will be kept + // althoug 0, 1 are touched twice (write & read) so + // they are in frequently used list, they are evicted because expiration + // and 2,3,4 will be kept for i, v := range []int{2, 3, 4} { if v != keys[i] { t.Fatalf("Expiring LRU eviction failed, expected keys {2,3,4} left, but found %v", elru.Keys()) @@ -369,7 +369,7 @@ func TestExpiring2Q_ExpireAfterWrite(t *testing.T) { // Test eviction by ExpireAfterAccess: basically same access sequence as above case // but different result because of ExpireAfterAccess func TestExpiring2Q_ExpireAfterAccess(t *testing.T) { - //use test timer for expiration + // use test timer for expiration tt := newTestTimer() elru, err := NewExpiring2Q(3, 30*time.Second, TimeTicker(tt.Now), ExpireAfterAccess) if err != nil { @@ -378,20 +378,20 @@ func TestExpiring2Q_ExpireAfterAccess(t *testing.T) { for i := 0; i < 2; i++ { elru.Add(i, i) } - //test timer ticks 20 seconds + // test timer ticks 20 seconds tt.Advance(20 * time.Second) - //add fresher entry <2,2> to cache + // add fresher entry <2,2> to cache elru.Add(2, 2) - //Get(0),Get(1) will move 0, 1 to freq-used list - //also moved them to back in expire list with newer timestamp - //2 will remain in recent-used list + // Get(0),Get(1) will move 0, 1 to freq-used list + // also moved them to back in expire list with newer timestamp + // 2 will remain in recent-used list for i := 0; i < 2; i++ { elru.Get(i) } - //test timer advance another 15 seconds, none expired - //and 2 in recent list + // test timer advance another 15 seconds, none expired + // and 2 in recent list tt.Advance(15 * time.Second) - //next add 3,4; verify 2,3 will be evicted, because 0,1 in freq list, not expired + // next add 3,4; verify 2,3 will be evicted, because 0,1 in freq list, not expired for i := 3; i < 5; i++ { elru.Add(i, i) } @@ -400,7 +400,7 @@ func TestExpiring2Q_ExpireAfterAccess(t *testing.T) { } keys := elru.Keys() sort.Slice(keys, func(i, j int) bool { return keys[i].(int) < keys[j].(int) }) - //and 0,1,4 will be kept + // and 0,1,4 will be kept for i, v := range []int{0, 1, 4} { if v != keys[i] { t.Fatalf("Expiring LRU eviction failed, expected keys {0,1,4} left, but found %v", elru.Keys()) @@ -410,7 +410,7 @@ func TestExpiring2Q_ExpireAfterAccess(t *testing.T) { // Test eviction by ExpireAfterWrite func TestExpiringARC_ExpireAfterWrite(t *testing.T) { - //use test timer for expiration + // use test timer for expiration tt := newTestTimer() elru, err := NewExpiringARC(3, 30*time.Second, TimeTicker(tt.Now)) if err != nil { @@ -419,19 +419,19 @@ func TestExpiringARC_ExpireAfterWrite(t *testing.T) { for i := 0; i < 2; i++ { elru.Add(i, i) } - //test timer ticks 20 seconds + // test timer ticks 20 seconds tt.Advance(20 * time.Second) - //add fresher entry <2,2> to cache + // add fresher entry <2,2> to cache elru.Add(2, 2) - //Get(0),Get(1) will move 0, 1 to freq-used list - //2 will remain in recent-used list + // Get(0),Get(1) will move 0, 1 to freq-used list + // 2 will remain in recent-used list for i := 0; i < 2; i++ { elru.Get(i) } - //test timer advance another 15 seconds, entries <0,0>,<1,1> timeout & expire now, - //so they should be evicted, although they are more recently retrieved than <2,2> + // test timer advance another 15 seconds, entries <0,0>,<1,1> timeout & expire now, + // so they should be evicted, although they are more recently retrieved than <2,2> tt.Advance(15 * time.Second) - //next add 3,4; verify 0,1 will be evicted + // next add 3,4; verify 0,1 will be evicted var ek, ev interface{} for i := 3; i < 5; i++ { evicted := elru.Add(i, i, &ek, &ev) @@ -445,9 +445,9 @@ func TestExpiringARC_ExpireAfterWrite(t *testing.T) { } keys := elru.Keys() sort.Slice(keys, func(i, j int) bool { return keys[i].(int) < keys[j].(int) }) - //althoug 0, 1 are touched twice (write & read) so - //they are in frequently used list, they are evicted because expiration - //and 2,3,4 will be kept + // althoug 0, 1 are touched twice (write & read) so + // they are in frequently used list, they are evicted because expiration + // and 2,3,4 will be kept for i, v := range []int{2, 3, 4} { if v != keys[i] { t.Fatalf("Expiring LRU eviction failed, expected keys {2,3,4} left, but found %v", elru.Keys()) @@ -458,7 +458,7 @@ func TestExpiringARC_ExpireAfterWrite(t *testing.T) { // Test eviction by ExpireAfterAccess: basically same access sequence as above case // but different result because of ExpireAfterAccess func TestExpiringARC_ExpireAfterAccess(t *testing.T) { - //use test timer for expiration + // use test timer for expiration tt := newTestTimer() elru, err := NewExpiringARC(3, 30*time.Second, TimeTicker(tt.Now), ExpireAfterAccess) if err != nil { @@ -467,20 +467,20 @@ func TestExpiringARC_ExpireAfterAccess(t *testing.T) { for i := 0; i < 2; i++ { elru.Add(i, i) } - //test timer ticks 20 seconds + // test timer ticks 20 seconds tt.Advance(20 * time.Second) - //add fresher entry <2,2> to cache + // add fresher entry <2,2> to cache elru.Add(2, 2) - //Get(0),Get(1) will move 0, 1 to freq-used list - //also moved them to back in expire list with newer timestamp - //2 will remain in recent-used list + // Get(0),Get(1) will move 0, 1 to freq-used list + // also moved them to back in expire list with newer timestamp + // 2 will remain in recent-used list for i := 0; i < 2; i++ { elru.Get(i) } - //test timer advance another 15 seconds, none expired - //and 2 in recent list + // test timer advance another 15 seconds, none expired + // and 2 in recent list tt.Advance(15 * time.Second) - //next add 3,4; verify 2,3 will be evicted, because 0,1 in freq list, not expired + // next add 3,4; verify 2,3 will be evicted, because 0,1 in freq list, not expired for i := 3; i < 5; i++ { elru.Add(i, i) } @@ -489,7 +489,7 @@ func TestExpiringARC_ExpireAfterAccess(t *testing.T) { } keys := elru.Keys() sort.Slice(keys, func(i, j int) bool { return keys[i].(int) < keys[j].(int) }) - //and 0,1,4 will be kept + // and 0,1,4 will be kept for i, v := range []int{0, 1, 4} { if v != keys[i] { t.Fatalf("Expiring LRU eviction failed, expected keys {0,1,4} left, but found %v", elru.Keys()) @@ -499,7 +499,7 @@ func TestExpiringARC_ExpireAfterAccess(t *testing.T) { // Test eviction by ExpireAfterWrite func TestExpiringLRU_ExpireAfterWrite(t *testing.T) { - //use test timer for expiration + // use test timer for expiration tt := newTestTimer() elru, err := NewExpiringLRU(3, 30*time.Second, TimeTicker(tt.Now)) if err != nil { @@ -508,19 +508,19 @@ func TestExpiringLRU_ExpireAfterWrite(t *testing.T) { for i := 0; i < 2; i++ { elru.Add(i, i) } - //test timer ticks 20 seconds + // test timer ticks 20 seconds tt.Advance(20 * time.Second) - //add fresher entry <2,2> to cache + // add fresher entry <2,2> to cache elru.Add(2, 2) - //Get(0),Get(1) will move 0, 1 to freq-used list - //2 will remain in recent-used list + // Get(0),Get(1) will move 0, 1 to freq-used list + // 2 will remain in recent-used list for i := 0; i < 2; i++ { elru.Get(i) } - //test timer advance another 15 seconds, entries <0,0>,<1,1> timeout & expire now, - //so they should be evicted, although they are more recently retrieved than <2,2> + // test timer advance another 15 seconds, entries <0,0>,<1,1> timeout & expire now, + // so they should be evicted, although they are more recently retrieved than <2,2> tt.Advance(15 * time.Second) - //next add 3,4; verify 0,1 will be evicted + // next add 3,4; verify 0,1 will be evicted var ek, ev interface{} for i := 3; i < 5; i++ { evicted := elru.Add(i, i, &ek, &ev) @@ -534,9 +534,9 @@ func TestExpiringLRU_ExpireAfterWrite(t *testing.T) { } keys := elru.Keys() sort.Slice(keys, func(i, j int) bool { return keys[i].(int) < keys[j].(int) }) - //althoug 0, 1 are touched twice (write & read) so - //they are in frequently used list, they are evicted because expiration - //and 2,3,4 will be kept + // althoug 0, 1 are touched twice (write & read) so + // they are in frequently used list, they are evicted because expiration + // and 2,3,4 will be kept for i, v := range []int{2, 3, 4} { if v != keys[i] { t.Fatalf("Expiring LRU eviction failed, expected keys {2,3,4} left, but found %v", elru.Keys()) @@ -547,7 +547,7 @@ func TestExpiringLRU_ExpireAfterWrite(t *testing.T) { // Test eviction by ExpireAfterAccess: basically same access sequence as above case // but different result because of ExpireAfterAccess func TestExpiringLRU_ExpireAfterAccess(t *testing.T) { - //use test timer for expiration + // use test timer for expiration tt := newTestTimer() elru, err := NewExpiringLRU(3, 30*time.Second, TimeTicker(tt.Now), ExpireAfterAccess) if err != nil { @@ -556,19 +556,19 @@ func TestExpiringLRU_ExpireAfterAccess(t *testing.T) { for i := 0; i < 2; i++ { elru.Add(i, i) } - //test timer ticks 20 seconds + // test timer ticks 20 seconds tt.Advance(20 * time.Second) - //add fresher entry <2,2> to cache + // add fresher entry <2,2> to cache elru.Add(2, 2) - //Get(0),Get(1) will move 0, 1 to back of access list - //also moved them to back in expire list with newer timestamp - //access list will be 2,0,1 + // Get(0),Get(1) will move 0, 1 to back of access list + // also moved them to back in expire list with newer timestamp + // access list will be 2,0,1 for i := 0; i < 2; i++ { elru.Get(i) } - //test timer advance another 15 seconds, none expired + // test timer advance another 15 seconds, none expired tt.Advance(15 * time.Second) - //next add 3,4; verify 2,0 will be evicted + // next add 3,4; verify 2,0 will be evicted for i := 3; i < 5; i++ { elru.Add(i, i) } @@ -577,7 +577,7 @@ func TestExpiringLRU_ExpireAfterAccess(t *testing.T) { } keys := elru.Keys() sort.Slice(keys, func(i, j int) bool { return keys[i].(int) < keys[j].(int) }) - //and 1,3,4 will be kept + // and 1,3,4 will be kept for i, v := range []int{1, 3, 4} { if v != keys[i] { t.Fatalf("Expiring LRU eviction failed, expected keys {1,3,4} left, but found %v", elru.Keys()) diff --git a/lru.go b/lru.go index cd7f99b..7bbcab0 100644 --- a/lru.go +++ b/lru.go @@ -23,7 +23,7 @@ func New(size int, opts ...Option) (*Cache, error) { // NewWithEvict constructs a fixed size cache with the given eviction // callback. func NewWithEvict(size int, onEvicted func(key interface{}, value interface{}), opts ...Option) (*Cache, error) { - //create a cache with default settings + // create a cache with default settings lru, err := simplelru.NewLRU(size, simplelru.EvictCallback(onEvicted)) if err != nil { return nil, err @@ -32,7 +32,7 @@ func NewWithEvict(size int, onEvicted func(key interface{}, value interface{}), lru: lru, lock: &sync.RWMutex{}, } - //apply options for custimization + // apply options for custimization for _, opt := range opts { if err = opt(c); err != nil { return nil, err From 986e48adc43f6a7868cefe72a945fd8fd80a2933 Mon Sep 17 00:00:00 2001 From: Yigong Liu Date: Sun, 22 Nov 2020 10:13:00 -0800 Subject: [PATCH 03/10] add missing sync in RemoveAllExpired() --- expiringlru.go | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/expiringlru.go b/expiringlru.go index 3708caf..6eeb9db 100644 --- a/expiringlru.go +++ b/expiringlru.go @@ -281,6 +281,13 @@ func (elru *ExpiringCache) Purge() { elru.lru.Purge() } +// RemoveAllExpired remove all expired entries, can be called by cleanup goroutine +func (elru *ExpiringCache) RemoveAllExpired() { + elru.lock.Lock() + defer elru.lock.Unlock() + elru.removeExpired(elru.timeNow(), true) +} + // either remove one (the oldest expired), or all expired func (elru *ExpiringCache) removeExpired(now time.Time, removeAllExpired bool) (res []*entry) { res = elru.expireList.RemoveExpired(now, removeAllExpired) @@ -290,11 +297,6 @@ func (elru *ExpiringCache) removeExpired(now time.Time, removeAllExpired bool) ( return } -// RemoveAllExpired remove all expired entries, can be called by cleanup goroutine -func (elru *ExpiringCache) RemoveAllExpired() { - elru.removeExpired(elru.timeNow(), true) -} - // oldest entries are at front of expire list type expireList struct { expList *list.List From c7c985f5f62ddcdac06ff48b4470002c378e3574 Mon Sep 17 00:00:00 2001 From: Yigong Liu Date: Sun, 22 Nov 2020 11:02:02 -0800 Subject: [PATCH 04/10] base expiringlru on simplelru.lru --- expiringlru.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/expiringlru.go b/expiringlru.go index 6eeb9db..f19cd50 100644 --- a/expiringlru.go +++ b/expiringlru.go @@ -5,6 +5,8 @@ import ( "fmt" "sync" "time" + + "github.com/hashicorp/golang-lru/simplelru" ) // common interface shared by 2q, arc and simple LRU, used as interface of backing LRU @@ -93,7 +95,7 @@ func NewExpiringARC(size int, expir time.Duration, opts ...OptionExp) (elru *Exp // size and entries lifetime duration, backed by a simple LRU func NewExpiringLRU(size int, expir time.Duration, opts ...OptionExp) (elru *ExpiringCache, err error) { // create a non synced LRU as backing store - lru, err := New(size, NoLock) + lru, err := simplelru.NewLRU(size, nil) if err != nil { return } From 5a93abcf4571c03d48094b8b629588ef9e41f81d Mon Sep 17 00:00:00 2001 From: Yigong Liu Date: Sun, 22 Nov 2020 12:16:43 -0800 Subject: [PATCH 05/10] following same pattern of simple LRU, separate 2q/arc Cache and LRU, make XXXCache just thread-safe wrapper over XXXLRU, avoid NoLock ugliness --- 2q.go | 180 ++------------------ 2q_test.go | 133 +-------------- arc.go | 217 ++---------------------- arc_test.go | 199 +--------------------- expiringlru.go | 16 +- lru.go | 26 +-- rwlocker.go | 24 --- simplelru/2q.go | 206 +++++++++++++++++++++++ simplelru/2q_test.go | 306 ++++++++++++++++++++++++++++++++++ simplelru/arc.go | 239 ++++++++++++++++++++++++++ simplelru/arc_test.go | 377 ++++++++++++++++++++++++++++++++++++++++++ 11 files changed, 1173 insertions(+), 750 deletions(-) delete mode 100644 rwlocker.go create mode 100644 simplelru/2q.go create mode 100644 simplelru/2q_test.go create mode 100644 simplelru/arc.go create mode 100644 simplelru/arc_test.go diff --git a/2q.go b/2q.go index 6006ffe..fc0912a 100644 --- a/2q.go +++ b/2q.go @@ -1,22 +1,11 @@ package lru import ( - "fmt" "sync" "github.com/hashicorp/golang-lru/simplelru" ) -const ( - // Default2QRecentRatio is the ratio of the 2Q cache dedicated - // to recently added entries that have only been accessed once. - Default2QRecentRatio = 0.25 - - // Default2QGhostEntries is the default ratio of ghost - // entries kept to track entries recently evicted - Default2QGhostEntries = 0.50 -) - // TwoQueueCache is a thread-safe fixed size 2Q cache. // 2Q is an enhancement over the standard LRU cache // in that it tracks both frequently and recently used @@ -27,168 +16,47 @@ const ( // head. The ARCCache is similar, but does not require setting any // parameters. type TwoQueueCache struct { - size int - recentSize int - - recent simplelru.LRUCache - frequent simplelru.LRUCache - recentEvict simplelru.LRUCache - lock RWLocker + lru *simplelru.TwoQueueLRU + lock sync.RWMutex } -// Option2Q define option to customize TwoQueueCache -type Option2Q func(c *TwoQueueCache) error - // New2Q creates a new TwoQueueCache using the default // values for the parameters. -func New2Q(size int, opts ...Option2Q) (*TwoQueueCache, error) { - return New2QParams(size, Default2QRecentRatio, Default2QGhostEntries, opts...) +func New2Q(size int) (*TwoQueueCache, error) { + return New2QParams(size, simplelru.Default2QRecentRatio, simplelru.Default2QGhostEntries) } // New2QParams creates a new TwoQueueCache using the provided // parameter values. -func New2QParams(size int, recentRatio, ghostRatio float64, opts ...Option2Q) (*TwoQueueCache, error) { - if size <= 0 { - return nil, fmt.Errorf("invalid size") - } - if recentRatio < 0.0 || recentRatio > 1.0 { - return nil, fmt.Errorf("invalid recent ratio") - } - if ghostRatio < 0.0 || ghostRatio > 1.0 { - return nil, fmt.Errorf("invalid ghost ratio") - } - - // Determine the sub-sizes - recentSize := int(float64(size) * recentRatio) - evictSize := int(float64(size) * ghostRatio) - - // Allocate the LRUs - recent, err := simplelru.NewLRU(size, nil) - if err != nil { - return nil, err - } - frequent, err := simplelru.NewLRU(size, nil) - if err != nil { - return nil, err - } - recentEvict, err := simplelru.NewLRU(evictSize, nil) +func New2QParams(size int, recentRatio, ghostRatio float64) (*TwoQueueCache, error) { + lru, err := simplelru.New2QParams(size, recentRatio, ghostRatio) if err != nil { return nil, err } - - // Initialize the cache - c := &TwoQueueCache{ - size: size, - recentSize: recentSize, - recent: recent, - frequent: frequent, - recentEvict: recentEvict, - lock: &sync.RWMutex{}, - } - // Apply options for customization - for _, opt := range opts { - if err = opt(c); err != nil { - return nil, err - } - } - return c, nil -} - -// NoLock2Q disables locking for TwoQueueCache -func NoLock2Q(c *TwoQueueCache) error { - c.lock = NoOpRWLocker{} - return nil + return &TwoQueueCache{ + lru: lru, + }, nil } // Get looks up a key's value from the cache. func (c *TwoQueueCache) Get(key interface{}) (value interface{}, ok bool) { c.lock.Lock() defer c.lock.Unlock() - - // Check if this is a frequent value - if val, ok := c.frequent.Get(key); ok { - return val, ok - } - - // If the value is contained in recent, then we - // promote it to frequent - if val, ok := c.recent.Peek(key); ok { - c.recent.Remove(key) - c.frequent.Add(key, val) - return val, ok - } - - // No hit - return nil, false + return c.lru.Get(key) } // Add adds a value to the cache, return evicted key/val if eviction happens. func (c *TwoQueueCache) Add(key, value interface{}, evictedKeyVal ...*interface{}) (evicted bool) { c.lock.Lock() defer c.lock.Unlock() - - // Check if the value is frequently used already, - // and just update the value - if c.frequent.Contains(key) { - c.frequent.Add(key, value) - return - } - - // Check if the value is recently used, and promote - // the value into the frequent list - if c.recent.Contains(key) { - c.recent.Remove(key) - c.frequent.Add(key, value) - return - } - - var evictedKey, evictedValue interface{} - // If the value was recently evicted, add it to the - // frequently used list - if c.recentEvict.Contains(key) { - evictedKey, evictedValue, evicted = c.ensureSpace(true) - c.recentEvict.Remove(key) - c.frequent.Add(key, value) - } else { - // Add to the recently seen list - evictedKey, evictedValue, evicted = c.ensureSpace(false) - c.recent.Add(key, value) - } - if evicted && len(evictedKeyVal) > 0 { - *evictedKeyVal[0] = evictedKey - } - if evicted && len(evictedKeyVal) > 1 { - *evictedKeyVal[1] = evictedValue - } - return evicted -} - -// ensureSpace is used to ensure we have space in the cache -func (c *TwoQueueCache) ensureSpace(recentEvict bool) (key, value interface{}, evicted bool) { - // If we have space, nothing to do - recentLen := c.recent.Len() - freqLen := c.frequent.Len() - if recentLen+freqLen < c.size { - return - } - - // If the recent buffer is larger than - // the target, evict from there - if recentLen > 0 && (recentLen > c.recentSize || (recentLen == c.recentSize && !recentEvict)) { - key, value, evicted = c.recent.RemoveOldest() - c.recentEvict.Add(key, nil) - return - } - - // Remove from the frequent list otherwise - return c.frequent.RemoveOldest() + return c.lru.Add(key, value, evictedKeyVal...) } // Len returns the number of items in the cache. func (c *TwoQueueCache) Len() int { c.lock.RLock() defer c.lock.RUnlock() - return c.recent.Len() + c.frequent.Len() + return c.lru.Len() } // Keys returns a slice of the keys in the cache. @@ -196,32 +64,21 @@ func (c *TwoQueueCache) Len() int { func (c *TwoQueueCache) Keys() []interface{} { c.lock.RLock() defer c.lock.RUnlock() - k1 := c.frequent.Keys() - k2 := c.recent.Keys() - return append(k1, k2...) + return c.lru.Keys() } // Remove removes the provided key from the cache. func (c *TwoQueueCache) Remove(key interface{}) bool { c.lock.Lock() defer c.lock.Unlock() - if c.frequent.Remove(key) { - return true - } - if c.recent.Remove(key) { - return true - } - c.recentEvict.Remove(key) - return false + return c.lru.Remove(key) } // Purge is used to completely clear the cache. func (c *TwoQueueCache) Purge() { c.lock.Lock() defer c.lock.Unlock() - c.recent.Purge() - c.frequent.Purge() - c.recentEvict.Purge() + c.lru.Purge() } // Contains is used to check if the cache contains a key @@ -229,7 +86,7 @@ func (c *TwoQueueCache) Purge() { func (c *TwoQueueCache) Contains(key interface{}) bool { c.lock.RLock() defer c.lock.RUnlock() - return c.frequent.Contains(key) || c.recent.Contains(key) + return c.lru.Contains(key) } // Peek is used to inspect the cache value of a key @@ -237,8 +94,5 @@ func (c *TwoQueueCache) Contains(key interface{}) bool { func (c *TwoQueueCache) Peek(key interface{}) (value interface{}, ok bool) { c.lock.RLock() defer c.lock.RUnlock() - if val, ok := c.frequent.Peek(key); ok { - return val, ok - } - return c.recent.Peek(key) + return c.lru.Peek(key) } diff --git a/2q_test.go b/2q_test.go index 1b0f351..32acbf1 100644 --- a/2q_test.go +++ b/2q_test.go @@ -86,140 +86,13 @@ func Test2Q_RandomOps(t *testing.T) { l.Remove(key) } - if l.recent.Len()+l.frequent.Len() > size { - t.Fatalf("bad: recent: %d freq: %d", - l.recent.Len(), l.frequent.Len()) + if l.Len() > size { + t.Fatalf("bad: expected %d, got %d", + size, l.Len()) } } } -func Test2Q_Get_RecentToFrequent(t *testing.T) { - l, err := New2Q(128) - if err != nil { - t.Fatalf("err: %v", err) - } - - // Touch all the entries, should be in t1 - for i := 0; i < 128; i++ { - l.Add(i, i) - } - if n := l.recent.Len(); n != 128 { - t.Fatalf("bad: %d", n) - } - if n := l.frequent.Len(); n != 0 { - t.Fatalf("bad: %d", n) - } - - // Get should upgrade to t2 - for i := 0; i < 128; i++ { - _, ok := l.Get(i) - if !ok { - t.Fatalf("missing: %d", i) - } - } - if n := l.recent.Len(); n != 0 { - t.Fatalf("bad: %d", n) - } - if n := l.frequent.Len(); n != 128 { - t.Fatalf("bad: %d", n) - } - - // Get be from t2 - for i := 0; i < 128; i++ { - _, ok := l.Get(i) - if !ok { - t.Fatalf("missing: %d", i) - } - } - if n := l.recent.Len(); n != 0 { - t.Fatalf("bad: %d", n) - } - if n := l.frequent.Len(); n != 128 { - t.Fatalf("bad: %d", n) - } -} - -func Test2Q_Add_RecentToFrequent(t *testing.T) { - l, err := New2Q(128) - if err != nil { - t.Fatalf("err: %v", err) - } - - // Add initially to recent - l.Add(1, 1) - if n := l.recent.Len(); n != 1 { - t.Fatalf("bad: %d", n) - } - if n := l.frequent.Len(); n != 0 { - t.Fatalf("bad: %d", n) - } - - // Add should upgrade to frequent - l.Add(1, 1) - if n := l.recent.Len(); n != 0 { - t.Fatalf("bad: %d", n) - } - if n := l.frequent.Len(); n != 1 { - t.Fatalf("bad: %d", n) - } - - // Add should remain in frequent - l.Add(1, 1) - if n := l.recent.Len(); n != 0 { - t.Fatalf("bad: %d", n) - } - if n := l.frequent.Len(); n != 1 { - t.Fatalf("bad: %d", n) - } -} - -func Test2Q_Add_RecentEvict(t *testing.T) { - l, err := New2Q(4) - if err != nil { - t.Fatalf("err: %v", err) - } - - // Add 1,2,3,4,5 -> Evict 1 - l.Add(1, 1) - l.Add(2, 2) - l.Add(3, 3) - l.Add(4, 4) - l.Add(5, 5) - if n := l.recent.Len(); n != 4 { - t.Fatalf("bad: %d", n) - } - if n := l.recentEvict.Len(); n != 1 { - t.Fatalf("bad: %d", n) - } - if n := l.frequent.Len(); n != 0 { - t.Fatalf("bad: %d", n) - } - - // Pull in the recently evicted - l.Add(1, 1) - if n := l.recent.Len(); n != 3 { - t.Fatalf("bad: %d", n) - } - if n := l.recentEvict.Len(); n != 1 { - t.Fatalf("bad: %d", n) - } - if n := l.frequent.Len(); n != 1 { - t.Fatalf("bad: %d", n) - } - - // Add 6, should cause another recent evict - l.Add(6, 6) - if n := l.recent.Len(); n != 3 { - t.Fatalf("bad: %d", n) - } - if n := l.recentEvict.Len(); n != 2 { - t.Fatalf("bad: %d", n) - } - if n := l.frequent.Len(); n != 1 { - t.Fatalf("bad: %d", n) - } -} - func Test2Q(t *testing.T) { l, err := New2Q(128) if err != nil { diff --git a/arc.go b/arc.go index 0770b70..6cf5bda 100644 --- a/arc.go +++ b/arc.go @@ -15,250 +15,64 @@ import ( // 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.LRUCache // T1 is the LRU for recently accessed items - b1 simplelru.LRUCache // B1 is the LRU for evictions from t1 - - t2 simplelru.LRUCache // T2 is the LRU for frequently accessed items - b2 simplelru.LRUCache // B2 is the LRU for evictions from t2 - - lock RWLocker + lru *simplelru.ARCLRU + lock sync.RWMutex } -// OptionARC defines option to customize ARCCache -type OptionARC func(*ARCCache) error - // NewARC creates an ARC of the given size -func NewARC(size int, opts ...OptionARC) (*ARCCache, error) { - // 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, nil) - if err != nil { - return nil, err - } - t2, err := simplelru.NewLRU(size, nil) +func NewARC(size int) (*ARCCache, error) { + lru, err := simplelru.NewARC(size) if err != nil { return nil, err } - // Initialize the ARC c := &ARCCache{ - size: size, - p: 0, - t1: t1, - b1: b1, - t2: t2, - b2: b2, - lock: &sync.RWMutex{}, - } - // Apply option settings - for _, opt := range opts { - if err = opt(c); err != nil { - return nil, err - } + lru: lru, } - return c, nil -} -// NoLockARC disables locking for ARCCache -func NoLockARC(c *ARCCache) error { - c.lock = NoOpRWLocker{} - return nil + 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.Remove(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 + return c.lru.Get(key) } // Add adds a value to the cache, return evicted key/val if it happens. func (c *ARCCache) Add(key, value interface{}, evictedKeyVal ...*interface{}) (evicted bool) { 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.Remove(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 - } - - var evictedKey, evictedValue interface{} - switch { - case c.b1.Contains(key): - // Check if this value was recently evicted as part of the - // recently used list - // 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 { - evictedKey, evictedValue, evicted = c.replace(false) - } - - // Remove from B1 - c.b1.Remove(key) - - // Add the key to the frequently used list - c.t2.Add(key, value) - - case c.b2.Contains(key): - // Check if this value was recently evicted as part of the - // frequently used list - // 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 { - evictedKey, evictedValue, evicted = c.replace(true) - } - - // Remove from B2 - c.b2.Remove(key) - - // Add the key to the frequently used list - c.t2.Add(key, value) - default: - // Brand new entry - // Potentially need to make room in the cache - if c.t1.Len()+c.t2.Len() >= c.size { - evictedKey, evictedValue, evicted = 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) - } - if evicted && len(evictedKeyVal) > 0 { - *evictedKeyVal[0] = evictedKey - } - if evicted && len(evictedKeyVal) > 1 { - *evictedKeyVal[1] = evictedValue - } - return evicted -} - -// 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) (k, v interface{}, ok bool) { - t1Len := c.t1.Len() - if t1Len > 0 && (t1Len > c.p || (t1Len == c.p && b2ContainsKey)) { - k, v, ok = c.t1.RemoveOldest() - if ok { - c.b1.Add(k, nil) - } - } else { - k, v, ok = c.t2.RemoveOldest() - if ok { - c.b2.Add(k, nil) - } - } - return + return c.lru.Add(key, value, evictedKeyVal...) } // 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() + return c.lru.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...) + return c.lru.Keys() } // Remove is used to purge a key from the cache func (c *ARCCache) Remove(key interface{}) bool { c.lock.Lock() defer c.lock.Unlock() - if c.t1.Remove(key) { - return true - } - if c.t2.Remove(key) { - return true - } - if c.b1.Remove(key) { - return false - } - if c.b2.Remove(key) { - return false - } - return false + return c.lru.Remove(key) } // Purge is used to clear the cache func (c *ARCCache) Purge() { c.lock.Lock() defer c.lock.Unlock() - c.t1.Purge() - c.t2.Purge() - c.b1.Purge() - c.b2.Purge() + c.lru.Purge() } // Contains is used to check if the cache contains a key @@ -266,7 +80,7 @@ func (c *ARCCache) Purge() { func (c *ARCCache) Contains(key interface{}) bool { c.lock.RLock() defer c.lock.RUnlock() - return c.t1.Contains(key) || c.t2.Contains(key) + return c.lru.Contains(key) } // Peek is used to inspect the cache value of a key @@ -274,8 +88,5 @@ func (c *ARCCache) Contains(key interface{}) bool { 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) + return c.lru.Peek(key) } diff --git a/arc_test.go b/arc_test.go index e2d9b68..54e8582 100644 --- a/arc_test.go +++ b/arc_test.go @@ -91,204 +91,11 @@ func TestARC_RandomOps(t *testing.T) { l.Remove(key) } - if l.t1.Len()+l.t2.Len() > size { - t.Fatalf("bad: t1: %d t2: %d b1: %d b2: %d p: %d", - l.t1.Len(), l.t2.Len(), l.b1.Len(), l.b2.Len(), l.p) + if l.Len() > size { + t.Fatalf("bad: got size %d, expected %d", + l.Len(), size) } - if l.b1.Len()+l.b2.Len() > size { - t.Fatalf("bad: t1: %d t2: %d b1: %d b2: %d p: %d", - l.t1.Len(), l.t2.Len(), l.b1.Len(), l.b2.Len(), l.p) - } - } -} - -func TestARC_Get_RecentToFrequent(t *testing.T) { - l, err := NewARC(128) - if err != nil { - t.Fatalf("err: %v", err) - } - - // Touch all the entries, should be in t1 - for i := 0; i < 128; i++ { - l.Add(i, i) - } - if n := l.t1.Len(); n != 128 { - t.Fatalf("bad: %d", n) - } - if n := l.t2.Len(); n != 0 { - t.Fatalf("bad: %d", n) - } - - // Get should upgrade to t2 - for i := 0; i < 128; i++ { - _, ok := l.Get(i) - if !ok { - t.Fatalf("missing: %d", i) - } - } - if n := l.t1.Len(); n != 0 { - t.Fatalf("bad: %d", n) - } - if n := l.t2.Len(); n != 128 { - t.Fatalf("bad: %d", n) - } - - // Get be from t2 - for i := 0; i < 128; i++ { - _, ok := l.Get(i) - if !ok { - t.Fatalf("missing: %d", i) - } - } - if n := l.t1.Len(); n != 0 { - t.Fatalf("bad: %d", n) - } - if n := l.t2.Len(); n != 128 { - t.Fatalf("bad: %d", n) - } -} - -func TestARC_Add_RecentToFrequent(t *testing.T) { - l, err := NewARC(128) - if err != nil { - t.Fatalf("err: %v", err) - } - - // Add initially to t1 - l.Add(1, 1) - if n := l.t1.Len(); n != 1 { - t.Fatalf("bad: %d", n) - } - if n := l.t2.Len(); n != 0 { - t.Fatalf("bad: %d", n) - } - - // Add should upgrade to t2 - l.Add(1, 1) - if n := l.t1.Len(); n != 0 { - t.Fatalf("bad: %d", n) } - if n := l.t2.Len(); n != 1 { - t.Fatalf("bad: %d", n) - } - - // Add should remain in t2 - l.Add(1, 1) - if n := l.t1.Len(); n != 0 { - t.Fatalf("bad: %d", n) - } - if n := l.t2.Len(); n != 1 { - t.Fatalf("bad: %d", n) - } -} - -func TestARC_Adaptive(t *testing.T) { - l, err := NewARC(4) - if err != nil { - t.Fatalf("err: %v", err) - } - - // Fill t1 - for i := 0; i < 4; i++ { - l.Add(i, i) - } - if n := l.t1.Len(); n != 4 { - t.Fatalf("bad: %d", n) - } - - // Move to t2 - l.Get(0) - l.Get(1) - if n := l.t2.Len(); n != 2 { - t.Fatalf("bad: %d", n) - } - - // Evict from t1 - l.Add(4, 4) - if n := l.b1.Len(); n != 1 { - t.Fatalf("bad: %d", n) - } - - // Current state - // t1 : (MRU) [4, 3] (LRU) - // t2 : (MRU) [1, 0] (LRU) - // b1 : (MRU) [2] (LRU) - // b2 : (MRU) [] (LRU) - - // Add 2, should cause hit on b1 - l.Add(2, 2) - if n := l.b1.Len(); n != 1 { - t.Fatalf("bad: %d", n) - } - if l.p != 1 { - t.Fatalf("bad: %d", l.p) - } - if n := l.t2.Len(); n != 3 { - t.Fatalf("bad: %d", n) - } - - // Current state - // t1 : (MRU) [4] (LRU) - // t2 : (MRU) [2, 1, 0] (LRU) - // b1 : (MRU) [3] (LRU) - // b2 : (MRU) [] (LRU) - - // Add 4, should migrate to t2 - l.Add(4, 4) - if n := l.t1.Len(); n != 0 { - t.Fatalf("bad: %d", n) - } - if n := l.t2.Len(); n != 4 { - t.Fatalf("bad: %d", n) - } - - // Current state - // t1 : (MRU) [] (LRU) - // t2 : (MRU) [4, 2, 1, 0] (LRU) - // b1 : (MRU) [3] (LRU) - // b2 : (MRU) [] (LRU) - - // Add 4, should evict to b2 - l.Add(5, 5) - if n := l.t1.Len(); n != 1 { - t.Fatalf("bad: %d", n) - } - if n := l.t2.Len(); n != 3 { - t.Fatalf("bad: %d", n) - } - if n := l.b2.Len(); n != 1 { - t.Fatalf("bad: %d", n) - } - - // Current state - // t1 : (MRU) [5] (LRU) - // t2 : (MRU) [4, 2, 1] (LRU) - // b1 : (MRU) [3] (LRU) - // b2 : (MRU) [0] (LRU) - - // Add 0, should decrease p - l.Add(0, 0) - if n := l.t1.Len(); n != 0 { - t.Fatalf("bad: %d", n) - } - if n := l.t2.Len(); n != 4 { - t.Fatalf("bad: %d", n) - } - if n := l.b1.Len(); n != 2 { - t.Fatalf("bad: %d", n) - } - if n := l.b2.Len(); n != 0 { - t.Fatalf("bad: %d", n) - } - if l.p != 0 { - t.Fatalf("bad: %d", l.p) - } - - // Current state - // t1 : (MRU) [] (LRU) - // t2 : (MRU) [0, 4, 2, 1] (LRU) - // b1 : (MRU) [5, 3] (LRU) - // b2 : (MRU) [0] (LRU) } func TestARC(t *testing.T) { diff --git a/expiringlru.go b/expiringlru.go index f19cd50..7836709 100644 --- a/expiringlru.go +++ b/expiringlru.go @@ -61,7 +61,7 @@ type ExpiringCache struct { expireType expiringType // placeholder for time.Now() for easier testing setup timeNow func() time.Time - lock RWLocker + lock sync.RWMutex } // OptionExp defines option to customize ExpiringCache @@ -70,8 +70,7 @@ type OptionExp func(c *ExpiringCache) error // NewExpiring2Q creates an expiring cache with specifized // size and entries lifetime duration, backed by a 2-queue LRU func NewExpiring2Q(size int, expir time.Duration, opts ...OptionExp) (elru *ExpiringCache, err error) { - // create a non synced LRU as backing store - lru, err := New2Q(size, NoLock2Q) + lru, err := simplelru.New2Q(size) if err != nil { return } @@ -82,8 +81,7 @@ func NewExpiring2Q(size int, expir time.Duration, opts ...OptionExp) (elru *Expi // NewExpiringARC creates an expiring cache with specifized // size and entries lifetime duration, backed by a ARC LRU func NewExpiringARC(size int, expir time.Duration, opts ...OptionExp) (elru *ExpiringCache, err error) { - // create a non synced LRU as backing store - lru, err := NewARC(size, NoLockARC) + lru, err := simplelru.NewARC(size) if err != nil { return } @@ -94,7 +92,6 @@ func NewExpiringARC(size int, expir time.Duration, opts ...OptionExp) (elru *Exp // NewExpiringLRU creates an expiring cache with specifized // size and entries lifetime duration, backed by a simple LRU func NewExpiringLRU(size int, expir time.Duration, opts ...OptionExp) (elru *ExpiringCache, err error) { - // create a non synced LRU as backing store lru, err := simplelru.NewLRU(size, nil) if err != nil { return @@ -113,7 +110,6 @@ func Expiring(expir time.Duration, lru lruCache, opts ...OptionExp) (*ExpiringCa expireList: newExpireList(), expireType: expireAfterWrite, timeNow: time.Now, - lock: &sync.RWMutex{}, } // apply options to customize for _, opt := range opts { @@ -124,12 +120,6 @@ func Expiring(expir time.Duration, lru lruCache, opts ...OptionExp) (*ExpiringCa return elru, nil } -// NoLockExp disables locking for ExpiringCache -func NoLockExp(elru *ExpiringCache) error { - elru.lock = NoOpRWLocker{} - return nil -} - // ExpireAfterWrite sets expiring policy func ExpireAfterWrite(elru *ExpiringCache) error { elru.expireType = expireAfterWrite diff --git a/lru.go b/lru.go index 7bbcab0..ee81bf6 100644 --- a/lru.go +++ b/lru.go @@ -9,44 +9,28 @@ import ( // Cache is a thread-safe fixed size LRU cache. type Cache struct { lru simplelru.LRUCache - lock RWLocker + lock sync.RWMutex } -// Option to customize LRUCache -type Option func(*Cache) error - // New creates an LRU of the given size. -func New(size int, opts ...Option) (*Cache, error) { - return NewWithEvict(size, nil, opts...) +func New(size int) (*Cache, error) { + return NewWithEvict(size, nil) } // NewWithEvict constructs a fixed size cache with the given eviction // callback. -func NewWithEvict(size int, onEvicted func(key interface{}, value interface{}), opts ...Option) (*Cache, error) { +func NewWithEvict(size int, onEvicted func(key interface{}, value interface{})) (*Cache, error) { // create a cache with default settings lru, err := simplelru.NewLRU(size, simplelru.EvictCallback(onEvicted)) if err != nil { return nil, err } c := &Cache{ - lru: lru, - lock: &sync.RWMutex{}, - } - // apply options for custimization - for _, opt := range opts { - if err = opt(c); err != nil { - return nil, err - } + lru: lru, } return c, nil } -// NoLock disables locking for LRUCache -func NoLock(c *Cache) error { - c.lock = NoOpRWLocker{} - return nil -} - // Purge is used to completely clear the cache. func (c *Cache) Purge() { c.lock.Lock() diff --git a/rwlocker.go b/rwlocker.go deleted file mode 100644 index 58012f8..0000000 --- a/rwlocker.go +++ /dev/null @@ -1,24 +0,0 @@ -package lru - -// RWLocker define base interface of sync.RWMutex -type RWLocker interface { - Lock() - Unlock() - RLock() - RUnlock() -} - -// NoOpRWLocker is a dummy noop implementation of RWLocker interface -type NoOpRWLocker struct{} - -// Lock perform noop Lock() operation -func (nop NoOpRWLocker) Lock() {} - -// Unlock perform noop Unlock() operation -func (nop NoOpRWLocker) Unlock() {} - -// RLock perform noop RLock() operation -func (nop NoOpRWLocker) RLock() {} - -// RUnlock perform noop RUnlock() operation -func (nop NoOpRWLocker) RUnlock() {} diff --git a/simplelru/2q.go b/simplelru/2q.go new file mode 100644 index 0000000..7f5111b --- /dev/null +++ b/simplelru/2q.go @@ -0,0 +1,206 @@ +package simplelru + +import ( + "fmt" +) + +const ( + // Default2QRecentRatio is the ratio of the 2Q cache dedicated + // to recently added entries that have only been accessed once. + Default2QRecentRatio = 0.25 + + // Default2QGhostEntries is the default ratio of ghost + // entries kept to track entries recently evicted + Default2QGhostEntries = 0.50 +) + +// TwoQueueLRU is a thread-safe fixed size 2Q LRU. +// 2Q is an enhancement over the standard LRU cache +// in that it tracks both frequently and recently used +// entries separately. This avoids a burst in access to new +// entries from evicting frequently used entries. It adds some +// additional tracking overhead to the standard LRU cache, and is +// computationally about 2x the cost, and adds some metadata over +// head. The ARCCache is similar, but does not require setting any +// parameters. +type TwoQueueLRU struct { + size int + recentSize int + + recent LRUCache + frequent LRUCache + recentEvict LRUCache +} + +// New2Q creates a new TwoQueueLRU using the default +// values for the parameters. +func New2Q(size int) (*TwoQueueLRU, error) { + return New2QParams(size, Default2QRecentRatio, Default2QGhostEntries) +} + +// New2QParams creates a new TwoQueueLRU using the provided +// parameter values. +func New2QParams(size int, recentRatio, ghostRatio float64) (*TwoQueueLRU, error) { + if size <= 0 { + return nil, fmt.Errorf("invalid size") + } + if recentRatio < 0.0 || recentRatio > 1.0 { + return nil, fmt.Errorf("invalid recent ratio") + } + if ghostRatio < 0.0 || ghostRatio > 1.0 { + return nil, fmt.Errorf("invalid ghost ratio") + } + + // Determine the sub-sizes + recentSize := int(float64(size) * recentRatio) + evictSize := int(float64(size) * ghostRatio) + + // Allocate the LRUs + recent, err := NewLRU(size, nil) + if err != nil { + return nil, err + } + frequent, err := NewLRU(size, nil) + if err != nil { + return nil, err + } + recentEvict, err := NewLRU(evictSize, nil) + if err != nil { + return nil, err + } + + // Initialize the cache + c := &TwoQueueLRU{ + size: size, + recentSize: recentSize, + recent: recent, + frequent: frequent, + recentEvict: recentEvict, + } + return c, nil +} + +// Get looks up a key's value from the cache. +func (c *TwoQueueLRU) Get(key interface{}) (value interface{}, ok bool) { + // Check if this is a frequent value + if val, ok := c.frequent.Get(key); ok { + return val, ok + } + + // If the value is contained in recent, then we + // promote it to frequent + if val, ok := c.recent.Peek(key); ok { + c.recent.Remove(key) + c.frequent.Add(key, val) + return val, ok + } + + // No hit + return nil, false +} + +// Add adds a value to the cache, return evicted key/val if eviction happens. +func (c *TwoQueueLRU) Add(key, value interface{}, evictedKeyVal ...*interface{}) (evicted bool) { + // Check if the value is frequently used already, + // and just update the value + if c.frequent.Contains(key) { + c.frequent.Add(key, value) + return + } + + // Check if the value is recently used, and promote + // the value into the frequent list + if c.recent.Contains(key) { + c.recent.Remove(key) + c.frequent.Add(key, value) + return + } + + var evictedKey, evictedValue interface{} + // If the value was recently evicted, add it to the + // frequently used list + if c.recentEvict.Contains(key) { + evictedKey, evictedValue, evicted = c.ensureSpace(true) + c.recentEvict.Remove(key) + c.frequent.Add(key, value) + } else { + // Add to the recently seen list + evictedKey, evictedValue, evicted = c.ensureSpace(false) + c.recent.Add(key, value) + } + if evicted && len(evictedKeyVal) > 0 { + *evictedKeyVal[0] = evictedKey + } + if evicted && len(evictedKeyVal) > 1 { + *evictedKeyVal[1] = evictedValue + } + return evicted +} + +// ensureSpace is used to ensure we have space in the cache +func (c *TwoQueueLRU) ensureSpace(recentEvict bool) (key, value interface{}, evicted bool) { + // If we have space, nothing to do + recentLen := c.recent.Len() + freqLen := c.frequent.Len() + if recentLen+freqLen < c.size { + return + } + + // If the recent buffer is larger than + // the target, evict from there + if recentLen > 0 && (recentLen > c.recentSize || (recentLen == c.recentSize && !recentEvict)) { + key, value, evicted = c.recent.RemoveOldest() + c.recentEvict.Add(key, nil) + return + } + + // Remove from the frequent list otherwise + return c.frequent.RemoveOldest() +} + +// Len returns the number of items in the cache. +func (c *TwoQueueLRU) Len() int { + return c.recent.Len() + c.frequent.Len() +} + +// Keys returns a slice of the keys in the cache. +// The frequently used keys are first in the returned slice. +func (c *TwoQueueLRU) Keys() []interface{} { + k1 := c.frequent.Keys() + k2 := c.recent.Keys() + return append(k1, k2...) +} + +// Remove removes the provided key from the cache. +func (c *TwoQueueLRU) Remove(key interface{}) bool { + if c.frequent.Remove(key) { + return true + } + if c.recent.Remove(key) { + return true + } + c.recentEvict.Remove(key) + return false +} + +// Purge is used to completely clear the cache. +func (c *TwoQueueLRU) Purge() { + c.recent.Purge() + c.frequent.Purge() + c.recentEvict.Purge() +} + +// Contains is used to check if the cache contains a key +// without updating recency or frequency. +func (c *TwoQueueLRU) Contains(key interface{}) bool { + return c.frequent.Contains(key) || c.recent.Contains(key) +} + +// Peek is used to inspect the cache value of a key +// without updating recency or frequency. +func (c *TwoQueueLRU) Peek(key interface{}) (value interface{}, ok bool) { + if val, ok := c.frequent.Peek(key); ok { + return val, ok + } + return c.recent.Peek(key) +} diff --git a/simplelru/2q_test.go b/simplelru/2q_test.go new file mode 100644 index 0000000..6ba575b --- /dev/null +++ b/simplelru/2q_test.go @@ -0,0 +1,306 @@ +package simplelru + +import ( + "math/rand" + "testing" +) + +func Benchmark2Q_Rand(b *testing.B) { + l, err := New2Q(8192) + if err != nil { + b.Fatalf("err: %v", err) + } + + trace := make([]int64, b.N*2) + for i := 0; i < b.N*2; i++ { + trace[i] = rand.Int63() % 32768 + } + + b.ResetTimer() + + var hit, miss int + for i := 0; i < 2*b.N; i++ { + if i%2 == 0 { + l.Add(trace[i], trace[i]) + } else { + _, ok := l.Get(trace[i]) + if ok { + hit++ + } else { + miss++ + } + } + } + b.Logf("hit: %d miss: %d ratio: %f", hit, miss, float64(hit)/float64(miss)) +} + +func Benchmark2Q_Freq(b *testing.B) { + l, err := New2Q(8192) + if err != nil { + b.Fatalf("err: %v", err) + } + + trace := make([]int64, b.N*2) + for i := 0; i < b.N*2; i++ { + if i%2 == 0 { + trace[i] = rand.Int63() % 16384 + } else { + trace[i] = rand.Int63() % 32768 + } + } + + b.ResetTimer() + + for i := 0; i < b.N; i++ { + l.Add(trace[i], trace[i]) + } + var hit, miss int + for i := 0; i < b.N; i++ { + _, ok := l.Get(trace[i]) + if ok { + hit++ + } else { + miss++ + } + } + b.Logf("hit: %d miss: %d ratio: %f", hit, miss, float64(hit)/float64(miss)) +} + +func Test2Q_RandomOps(t *testing.T) { + size := 128 + l, err := New2Q(128) + if err != nil { + t.Fatalf("err: %v", err) + } + + n := 200000 + for i := 0; i < n; i++ { + key := rand.Int63() % 512 + r := rand.Int63() + switch r % 3 { + case 0: + l.Add(key, key) + case 1: + l.Get(key) + case 2: + l.Remove(key) + } + + if l.recent.Len()+l.frequent.Len() > size { + t.Fatalf("bad: recent: %d freq: %d", + l.recent.Len(), l.frequent.Len()) + } + } +} + +func Test2Q_Get_RecentToFrequent(t *testing.T) { + l, err := New2Q(128) + if err != nil { + t.Fatalf("err: %v", err) + } + + // Touch all the entries, should be in t1 + for i := 0; i < 128; i++ { + l.Add(i, i) + } + if n := l.recent.Len(); n != 128 { + t.Fatalf("bad: %d", n) + } + if n := l.frequent.Len(); n != 0 { + t.Fatalf("bad: %d", n) + } + + // Get should upgrade to t2 + for i := 0; i < 128; i++ { + _, ok := l.Get(i) + if !ok { + t.Fatalf("missing: %d", i) + } + } + if n := l.recent.Len(); n != 0 { + t.Fatalf("bad: %d", n) + } + if n := l.frequent.Len(); n != 128 { + t.Fatalf("bad: %d", n) + } + + // Get be from t2 + for i := 0; i < 128; i++ { + _, ok := l.Get(i) + if !ok { + t.Fatalf("missing: %d", i) + } + } + if n := l.recent.Len(); n != 0 { + t.Fatalf("bad: %d", n) + } + if n := l.frequent.Len(); n != 128 { + t.Fatalf("bad: %d", n) + } +} + +func Test2Q_Add_RecentToFrequent(t *testing.T) { + l, err := New2Q(128) + if err != nil { + t.Fatalf("err: %v", err) + } + + // Add initially to recent + l.Add(1, 1) + if n := l.recent.Len(); n != 1 { + t.Fatalf("bad: %d", n) + } + if n := l.frequent.Len(); n != 0 { + t.Fatalf("bad: %d", n) + } + + // Add should upgrade to frequent + l.Add(1, 1) + if n := l.recent.Len(); n != 0 { + t.Fatalf("bad: %d", n) + } + if n := l.frequent.Len(); n != 1 { + t.Fatalf("bad: %d", n) + } + + // Add should remain in frequent + l.Add(1, 1) + if n := l.recent.Len(); n != 0 { + t.Fatalf("bad: %d", n) + } + if n := l.frequent.Len(); n != 1 { + t.Fatalf("bad: %d", n) + } +} + +func Test2Q_Add_RecentEvict(t *testing.T) { + l, err := New2Q(4) + if err != nil { + t.Fatalf("err: %v", err) + } + + // Add 1,2,3,4,5 -> Evict 1 + l.Add(1, 1) + l.Add(2, 2) + l.Add(3, 3) + l.Add(4, 4) + l.Add(5, 5) + if n := l.recent.Len(); n != 4 { + t.Fatalf("bad: %d", n) + } + if n := l.recentEvict.Len(); n != 1 { + t.Fatalf("bad: %d", n) + } + if n := l.frequent.Len(); n != 0 { + t.Fatalf("bad: %d", n) + } + + // Pull in the recently evicted + l.Add(1, 1) + if n := l.recent.Len(); n != 3 { + t.Fatalf("bad: %d", n) + } + if n := l.recentEvict.Len(); n != 1 { + t.Fatalf("bad: %d", n) + } + if n := l.frequent.Len(); n != 1 { + t.Fatalf("bad: %d", n) + } + + // Add 6, should cause another recent evict + l.Add(6, 6) + if n := l.recent.Len(); n != 3 { + t.Fatalf("bad: %d", n) + } + if n := l.recentEvict.Len(); n != 2 { + t.Fatalf("bad: %d", n) + } + if n := l.frequent.Len(); n != 1 { + t.Fatalf("bad: %d", n) + } +} + +func Test2Q(t *testing.T) { + l, err := New2Q(128) + if err != nil { + t.Fatalf("err: %v", err) + } + + for i := 0; i < 256; i++ { + l.Add(i, i) + } + if l.Len() != 128 { + t.Fatalf("bad len: %v", l.Len()) + } + + for i, k := range l.Keys() { + if v, ok := l.Get(k); !ok || v != k || v != i+128 { + t.Fatalf("bad key: %v", k) + } + } + for i := 0; i < 128; i++ { + _, ok := l.Get(i) + if ok { + t.Fatalf("should be evicted") + } + } + for i := 128; i < 256; i++ { + _, ok := l.Get(i) + if !ok { + t.Fatalf("should not be evicted") + } + } + for i := 128; i < 192; i++ { + l.Remove(i) + _, ok := l.Get(i) + if ok { + t.Fatalf("should be deleted") + } + } + + l.Purge() + if l.Len() != 0 { + t.Fatalf("bad len: %v", l.Len()) + } + if _, ok := l.Get(200); ok { + t.Fatalf("should contain nothing") + } +} + +// Test that Contains doesn't update recent-ness +func Test2Q_Contains(t *testing.T) { + l, err := New2Q(2) + if err != nil { + t.Fatalf("err: %v", err) + } + + l.Add(1, 1) + l.Add(2, 2) + if !l.Contains(1) { + t.Errorf("1 should be contained") + } + + l.Add(3, 3) + if l.Contains(1) { + t.Errorf("Contains should not have updated recent-ness of 1") + } +} + +// Test that Peek doesn't update recent-ness +func Test2Q_Peek(t *testing.T) { + l, err := New2Q(2) + if err != nil { + t.Fatalf("err: %v", err) + } + + l.Add(1, 1) + l.Add(2, 2) + if v, ok := l.Peek(1); !ok || v != 1 { + t.Errorf("1 should be set to 1: %v, %v", v, ok) + } + + l.Add(3, 3) + if l.Contains(1) { + t.Errorf("should not have updated recent-ness of 1") + } +} diff --git a/simplelru/arc.go b/simplelru/arc.go new file mode 100644 index 0000000..04e188c --- /dev/null +++ b/simplelru/arc.go @@ -0,0 +1,239 @@ +package simplelru + +// ARCLRU is a thread-safe fixed size Adaptive Replacement Cache LRU (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 ARCLRU struct { + size int // Size is the total capacity of the cache + p int // P is the dynamic preference towards T1 or T2 + + t1 LRUCache // T1 is the LRU for recently accessed items + b1 LRUCache // B1 is the LRU for evictions from t1 + + t2 LRUCache // T2 is the LRU for frequently accessed items + b2 LRUCache // B2 is the LRU for evictions from t2 +} + +// NewARC creates an ARC of the given size +func NewARC(size int) (*ARCLRU, error) { + // Create the sub LRUs + b1, err := NewLRU(size, nil) + if err != nil { + return nil, err + } + b2, err := NewLRU(size, nil) + if err != nil { + return nil, err + } + t1, err := NewLRU(size, nil) + if err != nil { + return nil, err + } + t2, err := NewLRU(size, nil) + if err != nil { + return nil, err + } + + // Initialize the ARC + c := &ARCLRU{ + 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 *ARCLRU) Get(key interface{}) (value interface{}, ok bool) { + // If the value is contained in T1 (recent), then + // promote it to T2 (frequent) + if val, ok := c.t1.Peek(key); ok { + c.t1.Remove(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, return evicted key/val if it happens. +func (c *ARCLRU) Add(key, value interface{}, evictedKeyVal ...*interface{}) (evicted bool) { + // Check if the value is contained in T1 (recent), and potentially + // promote it to frequent T2 + if c.t1.Contains(key) { + c.t1.Remove(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 + } + + var evictedKey, evictedValue interface{} + switch { + case c.b1.Contains(key): + // Check if this value was recently evicted as part of the + // recently used list + // 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 { + evictedKey, evictedValue, evicted = c.replace(false) + } + + // Remove from B1 + c.b1.Remove(key) + + // Add the key to the frequently used list + c.t2.Add(key, value) + + case c.b2.Contains(key): + // Check if this value was recently evicted as part of the + // frequently used list + // 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 { + evictedKey, evictedValue, evicted = c.replace(true) + } + + // Remove from B2 + c.b2.Remove(key) + + // Add the key to the frequently used list + c.t2.Add(key, value) + default: + // Brand new entry + // Potentially need to make room in the cache + if c.t1.Len()+c.t2.Len() >= c.size { + evictedKey, evictedValue, evicted = 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) + } + if evicted && len(evictedKeyVal) > 0 { + *evictedKeyVal[0] = evictedKey + } + if evicted && len(evictedKeyVal) > 1 { + *evictedKeyVal[1] = evictedValue + } + return evicted +} + +// replace is used to adaptively evict from either T1 or T2 +// based on the current learned value of P +func (c *ARCLRU) replace(b2ContainsKey bool) (k, v interface{}, ok bool) { + t1Len := c.t1.Len() + if t1Len > 0 && (t1Len > c.p || (t1Len == c.p && b2ContainsKey)) { + k, v, ok = c.t1.RemoveOldest() + if ok { + c.b1.Add(k, nil) + } + } else { + k, v, ok = c.t2.RemoveOldest() + if ok { + c.b2.Add(k, nil) + } + } + return +} + +// Len returns the number of cached entries +func (c *ARCLRU) Len() int { + return c.t1.Len() + c.t2.Len() +} + +// Keys returns all the cached keys +func (c *ARCLRU) Keys() []interface{} { + k1 := c.t1.Keys() + k2 := c.t2.Keys() + return append(k1, k2...) +} + +// Remove is used to purge a key from the cache +func (c *ARCLRU) Remove(key interface{}) bool { + if c.t1.Remove(key) { + return true + } + if c.t2.Remove(key) { + return true + } + if c.b1.Remove(key) { + return false + } + if c.b2.Remove(key) { + return false + } + return false +} + +// Purge is used to clear the cache +func (c *ARCLRU) Purge() { + c.t1.Purge() + c.t2.Purge() + c.b1.Purge() + c.b2.Purge() +} + +// Contains is used to check if the cache contains a key +// without updating recency or frequency. +func (c *ARCLRU) Contains(key interface{}) bool { + 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 *ARCLRU) Peek(key interface{}) (value interface{}, ok bool) { + if val, ok := c.t1.Peek(key); ok { + return val, ok + } + return c.t2.Peek(key) +} diff --git a/simplelru/arc_test.go b/simplelru/arc_test.go new file mode 100644 index 0000000..363b2f8 --- /dev/null +++ b/simplelru/arc_test.go @@ -0,0 +1,377 @@ +package simplelru + +import ( + "math/rand" + "testing" + "time" +) + +func init() { + rand.Seed(time.Now().Unix()) +} + +func BenchmarkARC_Rand(b *testing.B) { + l, err := NewARC(8192) + if err != nil { + b.Fatalf("err: %v", err) + } + + trace := make([]int64, b.N*2) + for i := 0; i < b.N*2; i++ { + trace[i] = rand.Int63() % 32768 + } + + b.ResetTimer() + + var hit, miss int + for i := 0; i < 2*b.N; i++ { + if i%2 == 0 { + l.Add(trace[i], trace[i]) + } else { + _, ok := l.Get(trace[i]) + if ok { + hit++ + } else { + miss++ + } + } + } + b.Logf("hit: %d miss: %d ratio: %f", hit, miss, float64(hit)/float64(miss)) +} + +func BenchmarkARC_Freq(b *testing.B) { + l, err := NewARC(8192) + if err != nil { + b.Fatalf("err: %v", err) + } + + trace := make([]int64, b.N*2) + for i := 0; i < b.N*2; i++ { + if i%2 == 0 { + trace[i] = rand.Int63() % 16384 + } else { + trace[i] = rand.Int63() % 32768 + } + } + + b.ResetTimer() + + for i := 0; i < b.N; i++ { + l.Add(trace[i], trace[i]) + } + var hit, miss int + for i := 0; i < b.N; i++ { + _, ok := l.Get(trace[i]) + if ok { + hit++ + } else { + miss++ + } + } + b.Logf("hit: %d miss: %d ratio: %f", hit, miss, float64(hit)/float64(miss)) +} + +func TestARC_RandomOps(t *testing.T) { + size := 128 + l, err := NewARC(128) + if err != nil { + t.Fatalf("err: %v", err) + } + + n := 200000 + for i := 0; i < n; i++ { + key := rand.Int63() % 512 + r := rand.Int63() + switch r % 3 { + case 0: + l.Add(key, key) + case 1: + l.Get(key) + case 2: + l.Remove(key) + } + + if l.t1.Len()+l.t2.Len() > size { + t.Fatalf("bad: t1: %d t2: %d b1: %d b2: %d p: %d", + l.t1.Len(), l.t2.Len(), l.b1.Len(), l.b2.Len(), l.p) + } + if l.b1.Len()+l.b2.Len() > size { + t.Fatalf("bad: t1: %d t2: %d b1: %d b2: %d p: %d", + l.t1.Len(), l.t2.Len(), l.b1.Len(), l.b2.Len(), l.p) + } + } +} + +func TestARC_Get_RecentToFrequent(t *testing.T) { + l, err := NewARC(128) + if err != nil { + t.Fatalf("err: %v", err) + } + + // Touch all the entries, should be in t1 + for i := 0; i < 128; i++ { + l.Add(i, i) + } + if n := l.t1.Len(); n != 128 { + t.Fatalf("bad: %d", n) + } + if n := l.t2.Len(); n != 0 { + t.Fatalf("bad: %d", n) + } + + // Get should upgrade to t2 + for i := 0; i < 128; i++ { + _, ok := l.Get(i) + if !ok { + t.Fatalf("missing: %d", i) + } + } + if n := l.t1.Len(); n != 0 { + t.Fatalf("bad: %d", n) + } + if n := l.t2.Len(); n != 128 { + t.Fatalf("bad: %d", n) + } + + // Get be from t2 + for i := 0; i < 128; i++ { + _, ok := l.Get(i) + if !ok { + t.Fatalf("missing: %d", i) + } + } + if n := l.t1.Len(); n != 0 { + t.Fatalf("bad: %d", n) + } + if n := l.t2.Len(); n != 128 { + t.Fatalf("bad: %d", n) + } +} + +func TestARC_Add_RecentToFrequent(t *testing.T) { + l, err := NewARC(128) + if err != nil { + t.Fatalf("err: %v", err) + } + + // Add initially to t1 + l.Add(1, 1) + if n := l.t1.Len(); n != 1 { + t.Fatalf("bad: %d", n) + } + if n := l.t2.Len(); n != 0 { + t.Fatalf("bad: %d", n) + } + + // Add should upgrade to t2 + l.Add(1, 1) + if n := l.t1.Len(); n != 0 { + t.Fatalf("bad: %d", n) + } + if n := l.t2.Len(); n != 1 { + t.Fatalf("bad: %d", n) + } + + // Add should remain in t2 + l.Add(1, 1) + if n := l.t1.Len(); n != 0 { + t.Fatalf("bad: %d", n) + } + if n := l.t2.Len(); n != 1 { + t.Fatalf("bad: %d", n) + } +} + +func TestARC_Adaptive(t *testing.T) { + l, err := NewARC(4) + if err != nil { + t.Fatalf("err: %v", err) + } + + // Fill t1 + for i := 0; i < 4; i++ { + l.Add(i, i) + } + if n := l.t1.Len(); n != 4 { + t.Fatalf("bad: %d", n) + } + + // Move to t2 + l.Get(0) + l.Get(1) + if n := l.t2.Len(); n != 2 { + t.Fatalf("bad: %d", n) + } + + // Evict from t1 + l.Add(4, 4) + if n := l.b1.Len(); n != 1 { + t.Fatalf("bad: %d", n) + } + + // Current state + // t1 : (MRU) [4, 3] (LRU) + // t2 : (MRU) [1, 0] (LRU) + // b1 : (MRU) [2] (LRU) + // b2 : (MRU) [] (LRU) + + // Add 2, should cause hit on b1 + l.Add(2, 2) + if n := l.b1.Len(); n != 1 { + t.Fatalf("bad: %d", n) + } + if l.p != 1 { + t.Fatalf("bad: %d", l.p) + } + if n := l.t2.Len(); n != 3 { + t.Fatalf("bad: %d", n) + } + + // Current state + // t1 : (MRU) [4] (LRU) + // t2 : (MRU) [2, 1, 0] (LRU) + // b1 : (MRU) [3] (LRU) + // b2 : (MRU) [] (LRU) + + // Add 4, should migrate to t2 + l.Add(4, 4) + if n := l.t1.Len(); n != 0 { + t.Fatalf("bad: %d", n) + } + if n := l.t2.Len(); n != 4 { + t.Fatalf("bad: %d", n) + } + + // Current state + // t1 : (MRU) [] (LRU) + // t2 : (MRU) [4, 2, 1, 0] (LRU) + // b1 : (MRU) [3] (LRU) + // b2 : (MRU) [] (LRU) + + // Add 4, should evict to b2 + l.Add(5, 5) + if n := l.t1.Len(); n != 1 { + t.Fatalf("bad: %d", n) + } + if n := l.t2.Len(); n != 3 { + t.Fatalf("bad: %d", n) + } + if n := l.b2.Len(); n != 1 { + t.Fatalf("bad: %d", n) + } + + // Current state + // t1 : (MRU) [5] (LRU) + // t2 : (MRU) [4, 2, 1] (LRU) + // b1 : (MRU) [3] (LRU) + // b2 : (MRU) [0] (LRU) + + // Add 0, should decrease p + l.Add(0, 0) + if n := l.t1.Len(); n != 0 { + t.Fatalf("bad: %d", n) + } + if n := l.t2.Len(); n != 4 { + t.Fatalf("bad: %d", n) + } + if n := l.b1.Len(); n != 2 { + t.Fatalf("bad: %d", n) + } + if n := l.b2.Len(); n != 0 { + t.Fatalf("bad: %d", n) + } + if l.p != 0 { + t.Fatalf("bad: %d", l.p) + } + + // Current state + // t1 : (MRU) [] (LRU) + // t2 : (MRU) [0, 4, 2, 1] (LRU) + // b1 : (MRU) [5, 3] (LRU) + // b2 : (MRU) [0] (LRU) +} + +func TestARC(t *testing.T) { + l, err := NewARC(128) + if err != nil { + t.Fatalf("err: %v", err) + } + + for i := 0; i < 256; i++ { + l.Add(i, i) + } + if l.Len() != 128 { + t.Fatalf("bad len: %v", l.Len()) + } + + for i, k := range l.Keys() { + if v, ok := l.Get(k); !ok || v != k || v != i+128 { + t.Fatalf("bad key: %v", k) + } + } + for i := 0; i < 128; i++ { + _, ok := l.Get(i) + if ok { + t.Fatalf("should be evicted") + } + } + for i := 128; i < 256; i++ { + _, ok := l.Get(i) + if !ok { + t.Fatalf("should not be evicted") + } + } + for i := 128; i < 192; i++ { + l.Remove(i) + _, ok := l.Get(i) + if ok { + t.Fatalf("should be deleted") + } + } + + l.Purge() + if l.Len() != 0 { + t.Fatalf("bad len: %v", l.Len()) + } + if _, ok := l.Get(200); ok { + t.Fatalf("should contain nothing") + } +} + +// Test that Contains doesn't update recent-ness +func TestARC_Contains(t *testing.T) { + l, err := NewARC(2) + if err != nil { + t.Fatalf("err: %v", err) + } + + l.Add(1, 1) + l.Add(2, 2) + if !l.Contains(1) { + t.Errorf("1 should be contained") + } + + l.Add(3, 3) + if l.Contains(1) { + t.Errorf("Contains should not have updated recent-ness of 1") + } +} + +// Test that Peek doesn't update recent-ness +func TestARC_Peek(t *testing.T) { + l, err := NewARC(2) + if err != nil { + t.Fatalf("err: %v", err) + } + + l.Add(1, 1) + l.Add(2, 2) + if v, ok := l.Peek(1); !ok || v != 1 { + t.Errorf("1 should be set to 1: %v, %v", v, ok) + } + + l.Add(3, 3) + if l.Contains(1) { + t.Errorf("should not have updated recent-ness of 1") + } +} From 410f6d2fc51b59b59592c7436652a6902667ad45 Mon Sep 17 00:00:00 2001 From: Yigong Liu Date: Sun, 22 Nov 2020 12:45:59 -0800 Subject: [PATCH 06/10] remove unnecessary interface redirection --- lru.go | 2 +- simplelru/2q.go | 6 +++--- simplelru/arc.go | 8 ++++---- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/lru.go b/lru.go index ee81bf6..44394ce 100644 --- a/lru.go +++ b/lru.go @@ -8,7 +8,7 @@ import ( // Cache is a thread-safe fixed size LRU cache. type Cache struct { - lru simplelru.LRUCache + lru *simplelru.LRU lock sync.RWMutex } diff --git a/simplelru/2q.go b/simplelru/2q.go index 7f5111b..3c0a7f5 100644 --- a/simplelru/2q.go +++ b/simplelru/2q.go @@ -27,9 +27,9 @@ type TwoQueueLRU struct { size int recentSize int - recent LRUCache - frequent LRUCache - recentEvict LRUCache + recent *LRU + frequent *LRU + recentEvict *LRU } // New2Q creates a new TwoQueueLRU using the default diff --git a/simplelru/arc.go b/simplelru/arc.go index 04e188c..72b9057 100644 --- a/simplelru/arc.go +++ b/simplelru/arc.go @@ -12,11 +12,11 @@ type ARCLRU struct { size int // Size is the total capacity of the cache p int // P is the dynamic preference towards T1 or T2 - t1 LRUCache // T1 is the LRU for recently accessed items - b1 LRUCache // B1 is the LRU for evictions from t1 + t1 *LRU // T1 is the LRU for recently accessed items + b1 *LRU // B1 is the LRU for evictions from t1 - t2 LRUCache // T2 is the LRU for frequently accessed items - b2 LRUCache // B2 is the LRU for evictions from t2 + t2 *LRU // T2 is the LRU for frequently accessed items + b2 *LRU // B2 is the LRU for evictions from t2 } // NewARC creates an ARC of the given size From 64aed772274bc683b1590a02ab9c62055d06048e Mon Sep 17 00:00:00 2001 From: Yigong Liu Date: Sun, 22 Nov 2020 16:46:41 -0800 Subject: [PATCH 07/10] cleanup code and revert 2q/arc to use lru interface --- expiringlru.go | 27 +++++++++++++++++---------- simplelru/2q.go | 6 +++--- simplelru/arc.go | 8 ++++---- 3 files changed, 24 insertions(+), 17 deletions(-) diff --git a/expiringlru.go b/expiringlru.go index 7836709..d0d6a43 100644 --- a/expiringlru.go +++ b/expiringlru.go @@ -55,6 +55,7 @@ const ( // expireAfterAccess and expireAfterWrite (default) // Internally keep a expireList sorted by entries' expirationTime type ExpiringCache struct { + size int lru lruCache expiration time.Duration expireList *expireList @@ -334,16 +335,22 @@ func (el *expireList) Remove(ent *entry) interface{} { // either remove one (the oldest expired), or remove all expired func (el *expireList) RemoveExpired(now time.Time, removeAllExpired bool) (res []*entry) { - for { - back := el.expList.Back() - if back == nil || back.Value.(*entry).expirationTime.After(now) { - break - } - // expired - ent := el.expList.Remove(back).(*entry) - res = append(res, ent) - if !removeAllExpired { - break + back := el.expList.Back() + if back == nil || back.Value.(*entry).expirationTime.After(now) { + return + } + // expired + ent := el.expList.Remove(back).(*entry) + res = append(res, ent) + if removeAllExpired { + for { + back = el.expList.Back() + if back == nil || back.Value.(*entry).expirationTime.After(now) { + break + } + // expired + ent := el.expList.Remove(back).(*entry) + res = append(res, ent) } } return diff --git a/simplelru/2q.go b/simplelru/2q.go index 3c0a7f5..7f5111b 100644 --- a/simplelru/2q.go +++ b/simplelru/2q.go @@ -27,9 +27,9 @@ type TwoQueueLRU struct { size int recentSize int - recent *LRU - frequent *LRU - recentEvict *LRU + recent LRUCache + frequent LRUCache + recentEvict LRUCache } // New2Q creates a new TwoQueueLRU using the default diff --git a/simplelru/arc.go b/simplelru/arc.go index 72b9057..04e188c 100644 --- a/simplelru/arc.go +++ b/simplelru/arc.go @@ -12,11 +12,11 @@ type ARCLRU struct { size int // Size is the total capacity of the cache p int // P is the dynamic preference towards T1 or T2 - t1 *LRU // T1 is the LRU for recently accessed items - b1 *LRU // B1 is the LRU for evictions from t1 + t1 LRUCache // T1 is the LRU for recently accessed items + b1 LRUCache // B1 is the LRU for evictions from t1 - t2 *LRU // T2 is the LRU for frequently accessed items - b2 *LRU // B2 is the LRU for evictions from t2 + t2 LRUCache // T2 is the LRU for frequently accessed items + b2 LRUCache // B2 is the LRU for evictions from t2 } // NewARC creates an ARC of the given size From 3204fcefa37d12c223328ef8a4708cd8023188c5 Mon Sep 17 00:00:00 2001 From: Yigong Liu Date: Sun, 22 Nov 2020 16:49:24 -0800 Subject: [PATCH 08/10] fix lint issue --- expiringlru.go | 1 - 1 file changed, 1 deletion(-) diff --git a/expiringlru.go b/expiringlru.go index d0d6a43..e014ca8 100644 --- a/expiringlru.go +++ b/expiringlru.go @@ -55,7 +55,6 @@ const ( // expireAfterAccess and expireAfterWrite (default) // Internally keep a expireList sorted by entries' expirationTime type ExpiringCache struct { - size int lru lruCache expiration time.Duration expireList *expireList From 20c1bf0e3579a8f38c6f2b956d69d384fffd0f42 Mon Sep 17 00:00:00 2001 From: Yigong Liu Date: Wed, 2 Dec 2020 20:42:37 -0800 Subject: [PATCH 09/10] based on review feedback, change RemoveAllExpired() to return expired key/values --- expiringlru.go | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/expiringlru.go b/expiringlru.go index e014ca8..2fa402c 100644 --- a/expiringlru.go +++ b/expiringlru.go @@ -274,10 +274,15 @@ func (elru *ExpiringCache) Purge() { } // RemoveAllExpired remove all expired entries, can be called by cleanup goroutine -func (elru *ExpiringCache) RemoveAllExpired() { +func (elru *ExpiringCache) RemoveAllExpired() (keys []interface{}, vals []interface{}) { elru.lock.Lock() defer elru.lock.Unlock() - elru.removeExpired(elru.timeNow(), true) + ents := elru.removeExpired(elru.timeNow(), true) + for _, ent := range ents { + keys = append(keys, ent.key) + vals = append(vals, ent.val) + } + return } // either remove one (the oldest expired), or all expired From f5f33c3f415faf5d98aaffa0242d625636e91bc3 Mon Sep 17 00:00:00 2001 From: Yigong Liu Date: Wed, 2 Dec 2020 21:41:24 -0800 Subject: [PATCH 10/10] make file names more consistent --- expiringlru.go => expiring.go | 0 expiringlru_test.go => expiring_test.go | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename expiringlru.go => expiring.go (100%) rename expiringlru_test.go => expiring_test.go (100%) diff --git a/expiringlru.go b/expiring.go similarity index 100% rename from expiringlru.go rename to expiring.go diff --git a/expiringlru_test.go b/expiring_test.go similarity index 100% rename from expiringlru_test.go rename to expiring_test.go