diff --git a/2q.go b/2q.go index 15fcad0..2ab2f7b 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,145 +16,69 @@ 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 sync.RWMutex + lru *simplelru.TwoQueueLRU + evictedKey, evictedVal interface{} + onEvictedCB func(k, v interface{}) + lock sync.RWMutex } // New2Q creates a new TwoQueueCache using the default // values for the parameters. func New2Q(size int) (*TwoQueueCache, error) { - return New2QParams(size, Default2QRecentRatio, Default2QGhostEntries) + return New2QParams(size, nil, simplelru.Default2QRecentRatio, simplelru.Default2QGhostEntries) +} + +// New2QWithEvict creates a new TwoQueueCache using the default +// values for the parameters and a callback to receive evicted values +func New2QWithEvict(size int, onEvict func(k, v interface{})) (*TwoQueueCache, error) { + return New2QParams(size, onEvict, simplelru.Default2QRecentRatio, simplelru.Default2QGhostEntries) } // New2QParams creates a new TwoQueueCache using the provided // parameter values. -func New2QParams(size int, recentRatio, ghostRatio float64) (*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) - if err != nil { - return nil, err +func New2QParams(size int, onEvict func(k, v interface{}), recentRatio, ghostRatio float64) (c *TwoQueueCache, err error) { + c = &TwoQueueCache{onEvictedCB: onEvict} + if onEvict != nil { + onEvict = c.onEvicted } + c.lru, err = simplelru.New2QParams(size, onEvict, recentRatio, ghostRatio) + return +} - // Initialize the cache - c := &TwoQueueCache{ - size: size, - recentSize: recentSize, - recent: recent, - frequent: frequent, - recentEvict: recentEvict, - } - return c, nil +// evicted key/val will be saved and sent thru registered callback +// outside of critical section later +func (c *TwoQueueCache) onEvicted(k, v interface{}) { + c.evictedKey = k + c.evictedVal = v } // 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. -func (c *TwoQueueCache) Add(key, value interface{}) { +// Add adds a value to the cache, return true if eviction happens. +func (c *TwoQueueCache) Add(key, value interface{}) (evicted bool) { + var ke, ve interface{} 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 - } - - // If the value was recently evicted, add it to the - // frequently used list - if c.recentEvict.Contains(key) { - c.ensureSpace(true) - c.recentEvict.Remove(key) - c.frequent.Add(key, value) - return - } - - // Add to the recently seen list - c.ensureSpace(false) - c.recent.Add(key, value) -} - -// ensureSpace is used to ensure we have space in the cache -func (c *TwoQueueCache) ensureSpace(recentEvict 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)) { - k, _, _ := c.recent.RemoveOldest() - c.recentEvict.Add(k, nil) - return - } - - // Remove from the frequent list otherwise - c.frequent.RemoveOldest() + evicted = c.lru.Add(key, value) + ke, ve = c.evictedKey, c.evictedVal + c.evictedKey = nil + c.evictedVal = nil + c.lock.Unlock() + if evicted && c.onEvictedCB != nil { + c.onEvictedCB(ke, ve) + } + return } // 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. @@ -173,33 +86,42 @@ 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{}) { +func (c *TwoQueueCache) Remove(key interface{}) (ok bool) { + var ke, ve interface{} c.lock.Lock() - defer c.lock.Unlock() - if c.frequent.Remove(key) { - return - } - if c.recent.Remove(key) { - return - } - if c.recentEvict.Remove(key) { - return - } + ok = c.lru.Remove(key) + ke, ve = c.evictedKey, c.evictedVal + c.evictedKey = nil + c.evictedVal = nil + c.lock.Unlock() + if ok && c.onEvictedCB != nil { + c.onEvictedCB(ke, ve) + } + return } // Purge is used to completely clear the cache. func (c *TwoQueueCache) Purge() { + var keys, vals []interface{} c.lock.Lock() - defer c.lock.Unlock() - c.recent.Purge() - c.frequent.Purge() - c.recentEvict.Purge() + if c.onEvictedCB != nil { + keys = c.lru.Keys() + for _, k := range keys { + val, _ := c.lru.Peek(k) + vals = append(vals, val) + } + } + c.lru.Purge() + c.lock.Unlock() + if c.onEvictedCB != nil { + for i := 0; i < len(keys); i++ { + c.onEvictedCB(keys[i], vals[i]) + } + } } // Contains is used to check if the cache contains a key @@ -207,7 +129,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 @@ -215,8 +137,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/README.md b/README.md index 33e58cf..0b7f8b3 100644 --- a/README.md +++ b/README.md @@ -23,3 +23,25 @@ if l.Len() != 128 { panic(fmt.Sprintf("bad len: %v", l.Len())) } ``` + +Or use expiring caches as following: + +```go +const EntryLifeTime = time.Minute +cache, _ := NewExpiringLRU(128, EntryLifeTime) +for i := 1; i < 256; i++ { + cache.Add(i, nil) +} +// and run a background goroutine to clean up expired entries aggressively +go func() { + LOOP: + for { + select { + case <-shutdown: + break LOOP + case <-time.Tick(EntryLifeTime): + cache.RemoveAllExpired() + } + } +}() +``` diff --git a/arc.go b/arc.go index e396f84..4cdd8dc 100644 --- a/arc.go +++ b/arc.go @@ -15,225 +15,103 @@ 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 sync.RWMutex + lru *simplelru.ARCLRU + evictedKey, evictedVal interface{} + onEvictedCB func(k, v interface{}) + lock sync.RWMutex } // NewARC creates an ARC of the given size func NewARC(size int) (*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) - if err != nil { - return nil, err - } + return NewARCWithEvict(size, nil) +} - // Initialize the ARC - c := &ARCCache{ - size: size, - p: 0, - t1: t1, - b1: b1, - t2: t2, - b2: b2, +// NewARCWithEvict creates an ARC of the given size and a callback to receive evicted values +func NewARCWithEvict(size int, onEvict func(k, v interface{})) (c *ARCCache, err error) { + c = &ARCCache{onEvictedCB: onEvict} + if onEvict != nil { + onEvict = c.onEvicted } - return c, nil + c.lru, err = simplelru.NewARCWithEvict(size, onEvict) + return +} + +// evicted key/val will be buffered and sent thru callback outside of critical section +func (c *ARCCache) onEvicted(k, v interface{}) { + c.evictedKey = k + c.evictedVal = v } // 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. -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{}) (evicted bool) { + var ke, ve interface{} c.lock.Lock() - defer c.lock.Unlock() - - // Check if the value is contained in T1 (recent), and potentially - // promote it to frequent T2 - if c.t1.Contains(key) { - c.t1.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 - } - - // Check if this value was recently evicted as part of the - // recently used list - if c.b1.Contains(key) { - // T1 set is too small, increase P appropriately - delta := 1 - b1Len := c.b1.Len() - b2Len := c.b2.Len() - if b2Len > b1Len { - delta = b2Len / b1Len - } - if c.p+delta >= c.size { - c.p = c.size - } else { - c.p += delta - } - - // Potentially need to make room in the cache - if c.t1.Len()+c.t2.Len() >= c.size { - c.replace(false) - } - - // Remove from B1 - c.b1.Remove(key) - - // Add the key to the frequently used list - c.t2.Add(key, value) - return - } - - // Check if this value was recently evicted as part of the - // frequently used list - if c.b2.Contains(key) { - // T2 set is too small, decrease P appropriately - delta := 1 - b1Len := c.b1.Len() - b2Len := c.b2.Len() - if b1Len > b2Len { - delta = b1Len / b2Len - } - if delta >= c.p { - c.p = 0 - } else { - c.p -= delta - } - - // Potentially need to make room in the cache - if c.t1.Len()+c.t2.Len() >= c.size { - c.replace(true) - } - - // Remove from B2 - c.b2.Remove(key) - - // Add the key to the frequently used list - c.t2.Add(key, value) - return - } - - // Potentially need to make room in the cache - if c.t1.Len()+c.t2.Len() >= c.size { - c.replace(false) - } - - // Keep the size of the ghost buffers trim - if c.b1.Len() > c.size-c.p { - c.b1.RemoveOldest() - } - if c.b2.Len() > c.p { - c.b2.RemoveOldest() - } - - // Add to the recently seen list - c.t1.Add(key, value) -} - -// replace is used to adaptively evict from either T1 or T2 -// based on the current learned value of P -func (c *ARCCache) replace(b2ContainsKey bool) { - t1Len := c.t1.Len() - if t1Len > 0 && (t1Len > c.p || (t1Len == c.p && b2ContainsKey)) { - k, _, ok := c.t1.RemoveOldest() - if ok { - c.b1.Add(k, nil) - } - } else { - k, _, ok := c.t2.RemoveOldest() - if ok { - c.b2.Add(k, nil) - } - } + evicted = c.lru.Add(key, value) + ke, ve = c.evictedKey, c.evictedVal + c.evictedKey = nil + c.evictedVal = nil + c.lock.Unlock() + if evicted && c.onEvictedCB != nil { + c.onEvictedCB(ke, ve) + } + return } // 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{}) { +func (c *ARCCache) Remove(key interface{}) (ok bool) { + var ke, ve interface{} c.lock.Lock() - defer c.lock.Unlock() - if c.t1.Remove(key) { - return - } - if c.t2.Remove(key) { - return - } - if c.b1.Remove(key) { - return - } - if c.b2.Remove(key) { - return - } + ok = c.lru.Remove(key) + ke, ve = c.evictedKey, c.evictedVal + c.evictedKey = nil + c.evictedVal = nil + c.lock.Unlock() + if ok && c.onEvictedCB != nil { + c.onEvictedCB(ke, ve) + } + return } // Purge is used to clear the cache func (c *ARCCache) Purge() { + var keys, vals []interface{} c.lock.Lock() - defer c.lock.Unlock() - c.t1.Purge() - c.t2.Purge() - c.b1.Purge() - c.b2.Purge() + if c.onEvictedCB != nil { + keys = c.lru.Keys() + for _, k := range keys { + val, _ := c.lru.Peek(k) + vals = append(vals, val) + } + } + c.lru.Purge() + c.lock.Unlock() + if c.onEvictedCB != nil { + for i := 0; i < len(keys); i++ { + c.onEvictedCB(keys[i], vals[i]) + } + } + } // Contains is used to check if the cache contains a key @@ -241,7 +119,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 @@ -249,8 +127,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/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/expiring.go b/expiring.go new file mode 100644 index 0000000..af9f0de --- /dev/null +++ b/expiring.go @@ -0,0 +1,414 @@ +package lru + +import ( + "container/list" + "fmt" + "sync" + "time" + + "github.com/hashicorp/golang-lru/simplelru" +) + +// 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 true if happened and + // updates the "recently used"-ness of the key. + Add(k, v 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 + evictedEntry *entry + onEvictedCB func(k, v interface{}) + // placeholder for time.Now() for easier testing setup + timeNow func() time.Time + lock sync.RWMutex +} + +// OptionExp defines options to customize ExpiringCache +type OptionExp func(c *ExpiringCache) error + +func newExpiringCacheWithOptions(expir time.Duration, opts []OptionExp) (elru *ExpiringCache, err error) { + // create expiring cache with default settings + elru = &ExpiringCache{ + expiration: expir, + expireList: newExpireList(), + expireType: expireAfterWrite, + timeNow: time.Now, + } + // apply options to customize + for _, opt := range opts { + if err = opt(elru); err != nil { + return + } + } + return +} + +// 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) { + if elru, err = newExpiringCacheWithOptions(expir, opts); err != nil { + return + } + elru.lru, err = simplelru.New2QWithEvict(size, elru.onEvicted) + if err != nil { + return + } + 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) { + if elru, err = newExpiringCacheWithOptions(expir, opts); err != nil { + return + } + elru.lru, err = simplelru.NewARCWithEvict(size, elru.onEvicted) + if err != nil { + return + } + 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) { + if elru, err = newExpiringCacheWithOptions(expir, opts); err != nil { + return + } + elru.lru, err = simplelru.NewLRU(size, elru.onEvicted) + if err != nil { + return + } + return +} + +// 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 +} + +// EvictedCallback register a callback to receive expired/evicted key, values +func EvictedCallback(cb func(k, v interface{})) OptionExp { + return func(elru *ExpiringCache) error { + elru.onEvictedCB = cb + 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 + } +} + +// buffer evicted key/val to be sent on registered callback +func (elru *ExpiringCache) onEvicted(k, v interface{}) { + elru.evictedEntry = v.(*entry) +} + +// Add add a key/val pair to cache with cache's default expiration duration +// return true if eviction happens. +// Should be used in most cases for better performance +func (elru *ExpiringCache) Add(k, v interface{}) (evicted bool) { + return elru.AddWithTTL(k, v, elru.expiration) +} + +// AddWithTTL add a key/val pair to cache with provided expiration duration +// return true if eviction happens. +// Using this with variant expiration durations could cause degraded performance +func (elru *ExpiringCache) AddWithTTL(k, v interface{}, expiration time.Duration) (evicted bool) { + elru.lock.Lock() + 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 + evicted = elru.lru.Add(k, ent) + var ke, ve interface{} + if evicted { + // remove evicted ent from expireList + ke, ve = elru.evictedEntry.key, elru.evictedEntry.val + elru.expireList.Remove(elru.evictedEntry) + elru.evictedEntry = nil + } else if len(expired) > 0 { + evicted = true + ke = expired[0].key + ve = expired[0].val + } + elru.lock.Unlock() + if evicted && elru.onEvictedCB != nil { + elru.onEvictedCB(ke, ve) + } + return evicted +} + +// 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{}) (ok bool) { + var ke, ve interface{} + elru.lock.Lock() + if ok = elru.lru.Remove(k); ok { + // there must be a eviction + elru.expireList.Remove(elru.evictedEntry) + ke, ve = elru.evictedEntry.key, elru.evictedEntry.val + elru.evictedEntry = nil + } + elru.lock.Unlock() + if ok && elru.onEvictedCB != nil { + elru.onEvictedCB(ke, ve) + } + return +} + +// 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() (res []interface{}) { + elru.lock.Lock() + // to get accurate key set, remove all expired + ents := elru.removeExpired(elru.timeNow(), true) + res = elru.lru.Keys() + elru.lock.Unlock() + if elru.onEvictedCB != nil { + for _, ent := range ents { + elru.onEvictedCB(ent.key, ent.val) + } + } + return +} + +// Len returns the number of items in the cache. +func (elru *ExpiringCache) Len() (sz int) { + elru.lock.Lock() + // to get accurate size, remove all expired + ents := elru.removeExpired(elru.timeNow(), true) + sz = elru.lru.Len() + elru.lock.Unlock() + if elru.onEvictedCB != nil { + for _, ent := range ents { + elru.onEvictedCB(ent.key, ent.val) + } + } + return +} + +// Purge is used to completely clear the cache. +func (elru *ExpiringCache) Purge() { + var ents []*entry + elru.lock.Lock() + if elru.onEvictedCB != nil { + ents = elru.expireList.AllEntries() + } + elru.lru.Purge() + elru.evictedEntry = nil + elru.expireList.Init() + elru.lock.Unlock() + if elru.onEvictedCB != nil { + for _, ent := range ents { + elru.onEvictedCB(ent.key, ent.val) + } + } +} + +// RemoveAllExpired remove all expired entries, can be called by cleanup goroutine +func (elru *ExpiringCache) RemoveAllExpired() { + elru.lock.Lock() + ents := elru.removeExpired(elru.timeNow(), true) + elru.lock.Unlock() + if elru.onEvictedCB != nil { + for _, ent := range ents { + elru.onEvictedCB(ent.key, ent.val) + } + } +} + +// 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) + } + // now here we already remove them from expireList, + // don't need to do it again + elru.evictedEntry = nil + return +} + +// 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) AllEntries() (res []*entry) { + for e := el.expList.Front(); e != nil; e = e.Next() { + res = append(res, e.Value.(*entry)) + } + return +} + +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) { + 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/expiring_test.go b/expiring_test.go new file mode 100644 index 0000000..6aa630e --- /dev/null +++ b/expiring_test.go @@ -0,0 +1,687 @@ +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) { + var ek, ev interface{} + elru, err := NewExpiring2Q(3, 30*time.Second, EvictedCallback(func(k, v interface{}) { + ek = k + ev = v + })) + 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 + for i := 3; i < 5; i++ { + evicted := elru.Add(i, i) + 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) { + var ek, ev interface{} + // use test timer for expiration + tt := newTestTimer() + elru, err := NewExpiring2Q(3, 30*time.Second, TimeTicker(tt.Now), EvictedCallback( + func(k, v interface{}) { + ek = k + ev = v + }, + )) + 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 + for i := 3; i < 5; i++ { + evicted := elru.Add(i, i) + 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) { + var ek, ev interface{} + // use test timer for expiration + tt := newTestTimer() + elru, err := NewExpiringARC(3, 30*time.Second, TimeTicker(tt.Now), EvictedCallback( + func(k, v interface{}) { + ek, ev = k, v + }, + )) + 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 + for i := 3; i < 5; i++ { + evicted := elru.Add(i, i) + 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) { + var ek, ev interface{} + // use test timer for expiration + tt := newTestTimer() + elru, err := NewExpiringLRU(3, 30*time.Second, TimeTicker(tt.Now), EvictedCallback( + func(k, v interface{}) { + ek, ev = k, v + }, + )) + 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 + for i := 3; i < 5; i++ { + evicted := elru.Add(i, i) + 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 9af03d4..895d8e3 100644 --- a/lru.go +++ b/lru.go @@ -167,7 +167,7 @@ func (c *Cache) Remove(key interface{}) (present bool) { } c.lock.Unlock() if c.onEvictedCB != nil && present { - c.onEvicted(k, v) + c.onEvictedCB(k, v) } return } diff --git a/simplelru/2q.go b/simplelru/2q.go new file mode 100644 index 0000000..c8ee569 --- /dev/null +++ b/simplelru/2q.go @@ -0,0 +1,228 @@ +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 + + onEvicted EvictCallback +} + +// New2Q creates a new TwoQueueLRU using the default +// values for the parameters. +func New2Q(size int) (*TwoQueueLRU, error) { + return New2QParams(size, nil, Default2QRecentRatio, Default2QGhostEntries) +} + +// New2QWithEvict creates a new TwoQueueLRU using the default +// values for the parameters and a callback to receive evicted values +func New2QWithEvict(size int, onEvict EvictCallback) (*TwoQueueLRU, error) { + return New2QParams(size, onEvict, Default2QRecentRatio, Default2QGhostEntries) +} + +// New2QParams creates a new TwoQueueLRU using the provided +// parameter values. +func New2QParams(size int, onEvict EvictCallback, 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, + onEvicted: onEvict, + } + 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{}) (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 && c.onEvicted != nil { + c.onEvicted(evictedKey, 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{}) (ok bool) { + var val interface{} + if val, ok = c.frequent.Peek(key); ok { + c.frequent.Remove(key) + } else if val, ok = c.recent.Peek(key); ok { + c.recent.Remove(key) + } + if ok { + if c.onEvicted != nil { + c.onEvicted(key, val) + } + } else { + c.recentEvict.Remove(key) + } + return +} + +// Purge is used to completely clear the cache. +func (c *TwoQueueLRU) Purge() { + if c.onEvicted != nil { + for _, k := range c.frequent.Keys() { + v, _ := c.frequent.Peek(k) + c.onEvicted(k, v) + } + for _, k := range c.recent.Keys() { + v, _ := c.recent.Peek(k) + c.onEvicted(k, v) + } + } + 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..f4e380f --- /dev/null +++ b/simplelru/arc.go @@ -0,0 +1,257 @@ +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 + + onEvicted EvictCallback +} + +// NewARC creates an ARC of the given size +func NewARC(size int) (*ARCLRU, error) { + return NewARCWithEvict(size, nil) +} + +// NewARCWithEvict creates an ARC of the given size and a callback to receive evicted values +func NewARCWithEvict(size int, onEvict EvictCallback) (*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, + onEvicted: onEvict, + } + 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{}) (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 && c.onEvicted != nil { + c.onEvicted(evictedKey, 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{}) (ok bool) { + var val interface{} + if val, ok = c.t1.Peek(key); ok { + c.t1.Remove(key) + } else if val, ok = c.t2.Peek(key); ok { + c.t2.Remove(key) + } + if ok { + if c.onEvicted != nil { + c.onEvicted(key, val) + } + } else { + if !c.b1.Remove(key) { + c.b2.Remove(key) + } + } + return +} + +// Purge is used to clear the cache +func (c *ARCLRU) Purge() { + if c.onEvicted != nil { + for _, k := range c.t1.Keys() { + v, _ := c.t1.Peek(k) + c.onEvicted(k, v) + } + for _, k := range c.t2.Keys() { + v, _ := c.t2.Peek(k) + c.onEvicted(k, v) + } + } + 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") + } +} diff --git a/simplelru/lru.go b/simplelru/lru.go index 9233583..9d3b030 100644 --- a/simplelru/lru.go +++ b/simplelru/lru.go @@ -5,9 +5,6 @@ import ( "errors" ) -// EvictCallback is used to get a callback when a cache entry is evicted -type EvictCallback func(key interface{}, value interface{}) - // LRU implements a non-thread safe fixed size LRU cache type LRU struct { size int @@ -48,7 +45,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{}) (evict bool) { // Check for existing item if ent, ok := c.items[key]; ok { c.evictList.MoveToFront(ent) @@ -61,12 +58,12 @@ 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() } - 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..f88df79 100644 --- a/simplelru/lru_interface.go +++ b/simplelru/lru_interface.go @@ -1,6 +1,9 @@ // Package simplelru provides simple LRU implementation based on build-in container/list. package simplelru +// EvictCallback is used to get a callback when a cache entry is evicted +type EvictCallback func(key interface{}, value interface{}) + // LRUCache is the interface for simple LRU cache. type LRUCache interface { // Adds a value to the cache, returns true if an eviction occurred and