diff --git a/2q.go b/2q.go index 15fcad0..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,145 +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 sync.RWMutex + lru *simplelru.TwoQueueLRU + 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, 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) + 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, - } - return c, 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. -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() - - // 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() + 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. @@ -173,33 +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{}) { +func (c *TwoQueueCache) Remove(key interface{}) bool { 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 - } + 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 @@ -207,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 @@ -215,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 e396f84..6cf5bda 100644 --- a/arc.go +++ b/arc.go @@ -15,47 +15,21 @@ 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 - + lru *simplelru.ARCLRU 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) + 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, + lru: lru, } + return c, nil } @@ -63,177 +37,42 @@ func NewARC(size int) (*ARCCache, error) { 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{}, 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 - } - - // 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) - } - } + 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{}) { +func (c *ARCCache) Remove(key interface{}) bool { 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 - } + 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 @@ -241,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 @@ -249,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/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..2fa402c --- /dev/null +++ b/expiring.go @@ -0,0 +1,361 @@ +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 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 sync.RWMutex +} + +// 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) { + lru, err := simplelru.New2Q(size) + 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) { + lru, err := simplelru.NewARC(size) + 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) { + lru, err := simplelru.NewLRU(size, nil) + 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, + } + // apply options to customize + for _, opt := range opts { + if err := opt(elru); err != nil { + return nil, err + } + } + return elru, 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 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{}) 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() +} + +// RemoveAllExpired remove all expired entries, can be called by cleanup goroutine +func (elru *ExpiringCache) RemoveAllExpired() (keys []interface{}, vals []interface{}) { + elru.lock.Lock() + defer elru.lock.Unlock() + 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 +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 +} + +// 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) { + 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..2540f33 --- /dev/null +++ b/expiring_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..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 } @@ -20,6 +20,7 @@ func New(size int) (*Cache, error) { // NewWithEvict constructs a fixed size cache with the given eviction // callback. 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 @@ -37,10 +38,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/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") + } +} 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 }