diff --git a/lru/lru.go b/lru/lru.go index e2699b2..161d231 100644 --- a/lru/lru.go +++ b/lru/lru.go @@ -5,7 +5,7 @@ import ( "sync" "time" - "github.com/ccheers/xpkg/generic/containerx/heap" + v2 "github.com/ccheers/xpkg/lru/v2" ) type ILRUCache interface { @@ -13,110 +13,51 @@ type ILRUCache interface { Get(ctx context.Context, key string) (interface{}, bool) } -type node struct { - expireAt time.Time - key string - value interface{} -} - type T struct { - heap *heap.Heap[*node] - objPool sync.Pool - + arcCache *v2.ARCCache latestGCAt time.Time - - gcLock sync.Mutex - - maxLen int - - mu sync.RWMutex - mm map[string]*node + mu sync.Mutex + mm map[string]time.Time } func NewLRUCache(maxLen int) ILRUCache { + cache, _ := v2.NewARC(int(uint32(maxLen))) return &T{ - heap: heap.New[*node](func(a, b *node) bool { - return a.expireAt.After(b.expireAt) - }), - objPool: sync.Pool{ - New: func() any { - return &node{} - }, - }, - maxLen: maxLen, - mm: make(map[string]*node, maxLen), + arcCache: cache, + latestGCAt: time.Unix(0, 0), + mm: make(map[string]time.Time, maxLen), } } func (x *T) Set(ctx context.Context, key string, value interface{}, expireAt time.Time) { defer x.gcTick() - x.mu.Lock() - node := x.objPool.Get().(*node) - node.expireAt = expireAt - node.key = key - node.value = value - - x.heap.Push(node) - x.mm[key] = node - - if len(x.mm) > x.maxLen { - node, ok := x.heap.Pop() - if ok { - delete(x.mm, node.key) - } - x.objPool.Put(node) - } + x.mm[key] = expireAt x.mu.Unlock() + x.arcCache.Add(key, value) } func (x *T) Get(ctx context.Context, key string) (interface{}, bool) { - defer x.gcTick() - - x.mu.RLock() - node, ok := x.mm[key] - x.mu.RUnlock() - if !ok { - return nil, false - } - if node.expireAt.Before(time.Now()) { - return nil, false - } - return node.value, ok + x.gcTick() + return x.arcCache.Get(key) } func (x *T) gcTick() { const calmDuration = time.Second * 10 - if !x.gcLock.TryLock() { + if !x.mu.TryLock() { return } - defer x.gcLock.Unlock() + defer x.mu.Unlock() now := time.Now() if now.Sub(x.latestGCAt) < calmDuration { return } - x.mu.Lock() - for { - node, ok := x.heap.Pop() - if !ok { - break - } - if node.expireAt.Before(now) { - delete(x.mm, node.key) - node.value = nil - x.objPool.Put(node) - } else { - x.heap.Push(node) - break + for key, t := range x.mm { + if t.Before(now) { + x.arcCache.Remove(key) + delete(x.mm, key) } } x.latestGCAt = now - mm := make(map[string]*node, len(x.mm)) - // 缩小 bucket 空隙 - for k, v := range x.mm { - mm[k] = v - } - x.mm = mm - x.mu.Unlock() } diff --git a/lru/lru_test.go b/lru/lru_test.go index 5d0d19d..9866308 100644 --- a/lru/lru_test.go +++ b/lru/lru_test.go @@ -27,3 +27,36 @@ func TestNewLRUCache(t *testing.T) { t.Fatal("should be empty") } } + +func BenchmarkLRUCache(b *testing.B) { + for i := 0; i < b.N; i++ { + cache := NewLRUCache(3) + ctx := context.TODO() + cache.Set(ctx, "1", 1, time.Now().Add(time.Second)) + cache.Set(ctx, "2", 2, time.Now().Add(time.Second)) + cache.Set(ctx, "3", 3, time.Now().Add(time.Second)) + cache.Set(ctx, "4", 4, time.Now().Add(time.Second)) + cache.Set(ctx, "5", 5, time.Now().Add(time.Second)) + cache.Set(ctx, "6", 6, time.Now().Add(time.Second)) + cache.Set(ctx, "7", 7, time.Now().Add(time.Second)) + cache.Set(ctx, "8", 8, time.Now().Add(time.Second)) + cache.Set(ctx, "9", 9, time.Now().Add(time.Second)) + cache.Set(ctx, "10", 10, time.Now().Add(time.Second)) + cache.Set(ctx, "11", 11, time.Now().Add(time.Second)) + cache.Set(ctx, "12", 12, time.Now().Add(time.Second)) + cache.Set(ctx, "13", 13, time.Now().Add(time.Second)) + cache.Set(ctx, "14", 14, time.Now().Add(time.Second)) + cache.Set(ctx, "15", 15, time.Now().Add(time.Second)) + cache.Set(ctx, "16", 16, time.Now().Add(time.Second)) + cache.Set(ctx, "17", 17, time.Now().Add(time.Second)) + cache.Set(ctx, "18", 18, time.Now().Add(time.Second)) + cache.Set(ctx, "19", 19, time.Now().Add(time.Second)) + cache.Set(ctx, "20", 20, time.Now().Add(time.Second)) + cache.Set(ctx, "21", 21, time.Now().Add(time.Second)) + cache.Set(ctx, "22", 22, time.Now().Add(time.Second)) + cache.Set(ctx, "23", 23, time.Now().Add(time.Second)) + cache.Set(ctx, "24", 24, time.Now().Add(time.Second)) + cache.Set(ctx, "25", 25, time.Now().Add(time.Second)) + cache.Set(ctx, "26", 26, time.Now().Add(time.Second)) + } +} diff --git a/lru/v2/arc.go b/lru/v2/arc.go new file mode 100644 index 0000000..9e1258d --- /dev/null +++ b/lru/v2/arc.go @@ -0,0 +1,268 @@ +package v2 + +import ( + "sync" +) + +// ARCCache is a thread-safe fixed size Adaptive Replacement Cache (ARC). +// ARC is an enhancement over the standard LRU cache in that tracks both +// frequency and recency of use. This avoids a burst in access to new +// entries from evicting the frequently used older entries. It adds some +// additional tracking overhead to a standard LRU cache, computationally +// it is roughly 2x the cost, and the extra memory overhead is linear +// with the size of the cache. ARC has been patented by IBM, but is +// similar to the TwoQueueCache (2Q) which requires setting parameters. +type ARCCache struct { + size int // Size is the total capacity of the cache + p int // P is the dynamic preference towards T1 or T2 + + t1 LRUCache[string, interface{}] // T1 is the LRU for recently accessed items + b1 LRUCache[string, struct{}] // B1 is the LRU for evictions from t1 + + t2 LRUCache[string, interface{}] // T2 is the LRU for frequently accessed items + b2 LRUCache[string, struct{}] // B2 is the LRU for evictions from t2 + + lock sync.RWMutex +} + +// NewARC creates an ARC of the given size +func NewARC(size int) (*ARCCache, error) { + // Create the sub LRUs + b1, err := NewLRU[string, struct{}](size, nil) + if err != nil { + return nil, err + } + b2, err := NewLRU[string, struct{}](size, nil) + if err != nil { + return nil, err + } + t1, err := NewLRU[string, interface{}](size, nil) + if err != nil { + return nil, err + } + t2, err := NewLRU[string, interface{}](size, nil) + if err != nil { + return nil, err + } + + // Initialize the ARC + x := &ARCCache{ + size: size, + p: 0, + t1: t1, + b1: b1, + t2: t2, + b2: b2, + } + return x, nil +} + +// Get looks up a key's value from the cache. +func (x *ARCCache) Get(key string) (value interface{}, ok bool) { + x.lock.Lock() + defer x.lock.Unlock() + + // If the value is contained in T1 (recent), then + // promote it to T2 (frequent) + if val, ok := x.t1.Peek(key); ok { + x.t1.Remove(key) + x.t2.Add(key, val) + return val, ok + } + + // Check if the value is contained in T2 (frequent) + if val, ok := x.t2.Get(key); ok { + return val, ok + } + + // No hit + return +} + +// Add adds a value to the cache. +func (x *ARCCache) Add(key string, value interface{}) { + x.lock.Lock() + defer x.lock.Unlock() + + // Check if the value is contained in T1 (recent), and potentially + // promote it to frequent T2 + if x.t1.Contains(key) { + x.t1.Remove(key) + x.t2.Add(key, value) + return + } + + // Check if the value is already in T2 (frequent) and update it + if x.t2.Contains(key) { + x.t2.Add(key, value) + return + } + + // Check if this value was recently evicted as part of the + // recently used list + if x.b1.Contains(key) { + // T1 set is too small, increase P appropriately + delta := 1 + b1Len := x.b1.Len() + b2Len := x.b2.Len() + if b2Len > b1Len { + delta = b2Len / b1Len + } + if x.p+delta >= x.size { + x.p = x.size + } else { + x.p += delta + } + + // Potentially need to make room in the cache + if x.t1.Len()+x.t2.Len() >= x.size { + x.replace(false) + } + + // Remove from B1 + x.b1.Remove(key) + + // Add the key to the frequently used list + x.t2.Add(key, value) + return + } + + // Check if this value was recently evicted as part of the + // frequently used list + if x.b2.Contains(key) { + // T2 set is too small, decrease P appropriately + delta := 1 + b1Len := x.b1.Len() + b2Len := x.b2.Len() + if b1Len > b2Len { + delta = b1Len / b2Len + } + if delta >= x.p { + x.p = 0 + } else { + x.p -= delta + } + + // Potentially need to make room in the cache + if x.t1.Len()+x.t2.Len() >= x.size { + x.replace(true) + } + + // Remove from B2 + x.b2.Remove(key) + + // Add the key to the frequently used list + x.t2.Add(key, value) + return + } + + // Potentially need to make room in the cache + if x.t1.Len()+x.t2.Len() >= x.size { + x.replace(false) + } + + // Keep the size of the ghost buffers trim + if x.b1.Len() > x.size-x.p { + x.b1.RemoveOldest() + } + if x.b2.Len() > x.p { + x.b2.RemoveOldest() + } + + // Add to the recently seen list + x.t1.Add(key, value) +} + +// replace is used to adaptively evict from either T1 or T2 +// based on the current learned value of P +func (x *ARCCache) replace(b2ContainsKey bool) { + t1Len := x.t1.Len() + if t1Len > 0 && (t1Len > x.p || (t1Len == x.p && b2ContainsKey)) { + k, _, ok := x.t1.RemoveOldest() + if ok { + x.b1.Add(k, struct{}{}) + } + } else { + k, _, ok := x.t2.RemoveOldest() + if ok { + x.b2.Add(k, struct{}{}) + } + } +} + +// Len returns the number of cached entries +func (x *ARCCache) Len() int { + x.lock.RLock() + defer x.lock.RUnlock() + return x.t1.Len() + x.t2.Len() +} + +// Cap returns the capacity of the cache +func (x *ARCCache) Cap() int { + return x.size +} + +// Keys returns all the cached keys +func (x *ARCCache) Keys() []string { + x.lock.RLock() + defer x.lock.RUnlock() + k1 := x.t1.Keys() + k2 := x.t2.Keys() + return append(k1, k2...) +} + +// Values returns all the cached values +func (x *ARCCache) Values() []interface{} { + x.lock.RLock() + defer x.lock.RUnlock() + v1 := x.t1.Values() + v2 := x.t2.Values() + return append(v1, v2...) +} + +// Remove is used to purge a key from the cache +func (x *ARCCache) Remove(key string) { + x.lock.Lock() + defer x.lock.Unlock() + if x.t1.Remove(key) { + return + } + if x.t2.Remove(key) { + return + } + if x.b1.Remove(key) { + return + } + if x.b2.Remove(key) { + return + } +} + +// Purge is used to clear the cache +func (x *ARCCache) Purge() { + x.lock.Lock() + defer x.lock.Unlock() + x.t1.Purge() + x.t2.Purge() + x.b1.Purge() + x.b2.Purge() +} + +// Contains is used to check if the cache contains a key +// without updating recency or frequency. +func (x *ARCCache) Contains(key string) bool { + x.lock.RLock() + defer x.lock.RUnlock() + return x.t1.Contains(key) || x.t2.Contains(key) +} + +// Peek is used to inspect the cache value of a key +// without updating recency or frequency. +func (x *ARCCache) Peek(key string) (value interface{}, ok bool) { + x.lock.RLock() + defer x.lock.RUnlock() + if val, ok := x.t1.Peek(key); ok { + return val, ok + } + return x.t2.Peek(key) +} diff --git a/lru/v2/define.go b/lru/v2/define.go new file mode 100644 index 0000000..6823317 --- /dev/null +++ b/lru/v2/define.go @@ -0,0 +1,227 @@ +package v2 + +import ( + "errors" + + "github.com/ccheers/xpkg/lru/v2/internal" +) + +var ( + ErrMustProvidePositiveSize = errors.New("must provide a positive size") +) + +// LRUCache is the interface for simple LRU cache. +type LRUCache[K comparable, V any] interface { + // Adds a value to the cache, returns true if an eviction occurred and + // updates the "recently used"-ness of the key. + Add(key K, value V) bool + + // Returns key's value from the cache and + // updates the "recently used"-ness of the key. #value, isFound + Get(key K) (value V, ok bool) + + // Checks if a key exists in cache without updating the recent-ness. + Contains(key K) (ok bool) + + // Returns key's value without updating the "recently used"-ness of the key. + Peek(key K) (value V, ok bool) + + // Removes a key from the cache. + Remove(key K) bool + + // Removes the oldest entry from cache. + RemoveOldest() (K, V, bool) + + // Returns the oldest entry from the cache. #key, value, isFound + GetOldest() (K, V, bool) + + // Returns a slice of the keys in the cache, from oldest to newest. + Keys() []K + + // Values returns a slice of the values in the cache, from oldest to newest. + Values() []V + + // Returns the number of items in the cache. + Len() int + + // Returns the capacity of the cache. + Cap() int + + // Clears all cache entries. + Purge() + + // Resizes cache, returning number evicted + Resize(int) int +} + +// EvictCallback is used to get a callback when a cache entry is evicted +type EvictCallback[K comparable, V any] func(key K, value V) + +// LRU implements a non-thread safe fixed size LRU cache +type LRU[K comparable, V any] struct { + size int + evictList *internal.LruList[K, V] + items map[K]*internal.Entry[K, V] + onEvict EvictCallback[K, V] +} + +// NewLRU constructs an LRU of the given size +func NewLRU[K comparable, V any](size int, onEvict EvictCallback[K, V]) (*LRU[K, V], error) { + if size <= 0 { + return nil, ErrMustProvidePositiveSize + } + + c := &LRU[K, V]{ + size: size, + evictList: internal.NewList[K, V](), + items: make(map[K]*internal.Entry[K, V]), + onEvict: onEvict, + } + return c, nil +} + +// Purge is used to completely clear the cache. +func (c *LRU[K, V]) Purge() { + for k, v := range c.items { + if c.onEvict != nil { + c.onEvict(k, v.Value) + } + delete(c.items, k) + } + c.evictList.Init() +} + +// Add adds a value to the cache. Returns true if an eviction occurred. +func (c *LRU[K, V]) Add(key K, value V) (evicted bool) { + // Check for existing item + if ent, ok := c.items[key]; ok { + c.evictList.MoveToFront(ent) + ent.Value = value + return false + } + + // Add new item + ent := c.evictList.PushFront(key, value) + c.items[key] = ent + + evict := c.evictList.Length() > c.size + // Verify size not exceeded + if evict { + c.removeOldest() + } + return evict +} + +// Get looks up a key's value from the cache. +func (c *LRU[K, V]) Get(key K) (value V, ok bool) { + if ent, ok := c.items[key]; ok { + c.evictList.MoveToFront(ent) + return ent.Value, true + } + return +} + +// Contains checks if a key is in the cache, without updating the recent-ness +// or deleting it for being stale. +func (c *LRU[K, V]) Contains(key K) (ok bool) { + _, ok = c.items[key] + return ok +} + +// Peek returns the key value (or undefined if not found) without updating +// the "recently used"-ness of the key. +func (c *LRU[K, V]) Peek(key K) (value V, ok bool) { + var ent *internal.Entry[K, V] + if ent, ok = c.items[key]; ok { + return ent.Value, true + } + return +} + +// Remove removes the provided key from the cache, returning if the +// key was contained. +func (c *LRU[K, V]) Remove(key K) (present bool) { + if ent, ok := c.items[key]; ok { + c.removeElement(ent) + return true + } + return false +} + +// RemoveOldest removes the oldest item from the cache. +func (c *LRU[K, V]) RemoveOldest() (key K, value V, ok bool) { + if ent := c.evictList.Back(); ent != nil { + c.removeElement(ent) + return ent.Key, ent.Value, true + } + return +} + +// GetOldest returns the oldest entry +func (c *LRU[K, V]) GetOldest() (key K, value V, ok bool) { + if ent := c.evictList.Back(); ent != nil { + return ent.Key, ent.Value, true + } + return +} + +// Keys returns a slice of the keys in the cache, from oldest to newest. +func (c *LRU[K, V]) Keys() []K { + keys := make([]K, c.evictList.Length()) + i := 0 + for ent := c.evictList.Back(); ent != nil; ent = ent.PrevEntry() { + keys[i] = ent.Key + i++ + } + return keys +} + +// Values returns a slice of the values in the cache, from oldest to newest. +func (c *LRU[K, V]) Values() []V { + values := make([]V, len(c.items)) + i := 0 + for ent := c.evictList.Back(); ent != nil; ent = ent.PrevEntry() { + values[i] = ent.Value + i++ + } + return values +} + +// Len returns the number of items in the cache. +func (c *LRU[K, V]) Len() int { + return c.evictList.Length() +} + +// Cap returns the capacity of the cache +func (c *LRU[K, V]) Cap() int { + return c.size +} + +// Resize changes the cache size. +func (c *LRU[K, V]) Resize(size int) (evicted int) { + diff := c.Len() - size + if diff < 0 { + diff = 0 + } + for i := 0; i < diff; i++ { + c.removeOldest() + } + c.size = size + return diff +} + +// removeOldest removes the oldest item from the cache. +func (c *LRU[K, V]) removeOldest() { + if ent := c.evictList.Back(); ent != nil { + c.removeElement(ent) + } +} + +// removeElement is used to remove a given list element from the cache +func (c *LRU[K, V]) removeElement(e *internal.Entry[K, V]) { + c.evictList.Remove(e) + delete(c.items, e.Key) + if c.onEvict != nil { + c.onEvict(e.Key, e.Value) + } +} diff --git a/lru/v2/internal/list.go b/lru/v2/internal/list.go new file mode 100644 index 0000000..5cd74a0 --- /dev/null +++ b/lru/v2/internal/list.go @@ -0,0 +1,142 @@ +// Copyright 2009 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE_list file. + +package internal + +import "time" + +// Entry is an LRU Entry +type Entry[K comparable, V any] struct { + // Next and previous pointers in the doubly-linked list of elements. + // To simplify the implementation, internally a list l is implemented + // as a ring, such that &l.root is both the next element of the last + // list element (l.Back()) and the previous element of the first list + // element (l.Front()). + next, prev *Entry[K, V] + + // The list to which this element belongs. + list *LruList[K, V] + + // The LRU Key of this element. + Key K + + // The Value stored with this element. + Value V + + // The time this element would be cleaned up, optional + ExpiresAt time.Time + + // The expiry bucket item was put in, optional + ExpireBucket uint8 +} + +// PrevEntry returns the previous list element or nil. +func (e *Entry[K, V]) PrevEntry() *Entry[K, V] { + if p := e.prev; e.list != nil && p != &e.list.root { + return p + } + return nil +} + +// LruList represents a doubly linked list. +// The zero Value for LruList is an empty list ready to use. +type LruList[K comparable, V any] struct { + root Entry[K, V] // sentinel list element, only &root, root.prev, and root.next are used + len int // current list Length excluding (this) sentinel element +} + +// Init initializes or clears list l. +func (l *LruList[K, V]) Init() *LruList[K, V] { + l.root.next = &l.root + l.root.prev = &l.root + l.len = 0 + return l +} + +// NewList returns an initialized list. +func NewList[K comparable, V any]() *LruList[K, V] { return new(LruList[K, V]).Init() } + +// Length returns the number of elements of list l. +// The complexity is O(1). +func (l *LruList[K, V]) Length() int { return l.len } + +// Back returns the last element of list l or nil if the list is empty. +func (l *LruList[K, V]) Back() *Entry[K, V] { + if l.len == 0 { + return nil + } + return l.root.prev +} + +// lazyInit lazily initializes a zero List Value. +func (l *LruList[K, V]) lazyInit() { + if l.root.next == nil { + l.Init() + } +} + +// insert inserts e after at, increments l.len, and returns e. +func (l *LruList[K, V]) insert(e, at *Entry[K, V]) *Entry[K, V] { + e.prev = at + e.next = at.next + e.prev.next = e + e.next.prev = e + e.list = l + l.len++ + return e +} + +// insertValue is a convenience wrapper for insert(&Entry{Value: v, ExpiresAt: ExpiresAt}, at). +func (l *LruList[K, V]) insertValue(k K, v V, expiresAt time.Time, at *Entry[K, V]) *Entry[K, V] { + return l.insert(&Entry[K, V]{Value: v, Key: k, ExpiresAt: expiresAt}, at) +} + +// Remove removes e from its list, decrements l.len +func (l *LruList[K, V]) Remove(e *Entry[K, V]) V { + e.prev.next = e.next + e.next.prev = e.prev + e.next = nil // avoid memory leaks + e.prev = nil // avoid memory leaks + e.list = nil + l.len-- + + return e.Value +} + +// move moves e to next to at. +func (l *LruList[K, V]) move(e, at *Entry[K, V]) { + if e == at { + return + } + e.prev.next = e.next + e.next.prev = e.prev + + e.prev = at + e.next = at.next + e.prev.next = e + e.next.prev = e +} + +// PushFront inserts a new element e with value v at the front of list l and returns e. +func (l *LruList[K, V]) PushFront(k K, v V) *Entry[K, V] { + l.lazyInit() + return l.insertValue(k, v, time.Time{}, &l.root) +} + +// PushFrontExpirable inserts a new expirable element e with Value v at the front of list l and returns e. +func (l *LruList[K, V]) PushFrontExpirable(k K, v V, expiresAt time.Time) *Entry[K, V] { + l.lazyInit() + return l.insertValue(k, v, expiresAt, &l.root) +} + +// MoveToFront moves element e to the front of list l. +// If e is not an element of l, the list is not modified. +// The element must not be nil. +func (l *LruList[K, V]) MoveToFront(e *Entry[K, V]) { + if e.list != l || l.root.next == e { + return + } + // see comment in List.Remove about initialization of l + l.move(e, &l.root) +}