// sieve.go - SIEVE - a simple and efficient cache // // (c) 2024 Sudhi Herle // // Copyright 2024- Sudhi Herle // License: BSD-2-Clause // // If you need a commercial license for this work, please contact // the author. // // This software does not come with any express or implied // warranty; it is provided "as is". No claim is made to its // suitability for any purpose. // This is golang implementation of the SIEVE cache eviction algorithm // The original paper is: // https://yazhuozhang.com/assets/pdf/nsdi24-sieve.pdf // // This implementation closely follows the paper - but uses golang generics // for an ergonomic interface. // Package sieve implements the SIEVE cache eviction algorithm. // SIEVE stands in contrast to other eviction algorithms like LRU, 2Q, ARC // with its simplicity. The original paper is in: // https://yazhuozhang.com/assets/pdf/nsdi24-sieve.pdf // // SIEVE is built on a FIFO queue - with an extra pointer (called "hand") in // the paper. This "hand" plays a crucial role in determining who to evict // next. package sieve import ( "fmt" "strings" "sync" "sync/atomic" ) // node contains the tuple as a node in a linked list. type node[K comparable, V any] struct { sync.Mutex key K val V visited atomic.Bool next *node[K, V] prev *node[K, V] } // Sieve represents a cache mapping the key of type 'K' with // a value of type 'V'. The type 'K' must implement the // comparable trait. An instance of Sieve has a fixed max capacity; // new additions to the cache beyond the capacity will cause cache // eviction of other entries - as determined by the SIEVE algorithm. type Sieve[K comparable, V any] struct { mu sync.Mutex cache *syncMap[K, *node[K, V]] head *node[K, V] tail *node[K, V] hand *node[K, V] size int capacity int pool *syncPool[node[K, V]] } // New creates a new cache of size 'capacity' mapping key 'K' to value 'V' func New[K comparable, V any](capacity int) *Sieve[K, V] { s := &Sieve[K, V]{ cache: newSyncMap[K, *node[K, V]](), capacity: capacity, pool: newSyncPool[node[K, V]](), } return s } // Get fetches the value for a given key in the cache. // It returns true if the key is in the cache, false otherwise. // The zero value for 'V' is returned when key is not in the cache. func (s *Sieve[K, V]) Get(key K) (V, bool) { if v, ok := s.cache.Get(key); ok { v.visited.Store(true) return v.val, true } var x V return x, false } // Add adds a new element to the cache or overwrite one if it exists // Return true if we replaced, false otherwise func (s *Sieve[K, V]) Add(key K, val V) bool { if v, ok := s.cache.Get(key); ok { v.visited.Store(true) v.Lock() v.val = val v.Unlock() return true } s.mu.Lock() s.add(key, val) s.mu.Unlock() return false } // Probe adds if not present in the cache. // Returns: // // when key is present in the cache // when key is not present in the cache func (s *Sieve[K, V]) Probe(key K, val V) (V, bool) { if v, ok := s.cache.Get(key); ok { v.visited.Store(true) return v.val, true } s.mu.Lock() s.add(key, val) s.mu.Unlock() return val, false } // Delete deletes the named key from the cache // It returns true if the item was in the cache and false otherwise func (s *Sieve[K, V]) Delete(key K) bool { if v, ok := s.cache.Del(key); ok { s.mu.Lock() s.remove(v) s.mu.Unlock() return true } return false } // Purge resets the cache func (s *Sieve[K, V]) Purge() { s.mu.Lock() s.cache = newSyncMap[K, *node[K, V]]() s.head = nil s.tail = nil s.mu.Unlock() } // Len returns the current cache utilization func (s *Sieve[K, V]) Len() int { return s.size } // Cap returns the max cache capacity func (s *Sieve[K, V]) Cap() int { return s.capacity } // String returns a string description of the sieve cache func (s *Sieve[K, V]) String() string { s.mu.Lock() m := s.desc() s.mu.Unlock() return m } // Dump dumps all the cache contents as a newline delimited // string. func (s *Sieve[K, V]) Dump() string { var b strings.Builder s.mu.Lock() b.WriteString(s.desc()) b.WriteRune('\n') for n := s.head; n != nil; n = n.next { h := " " if n == s.hand { h = ">>" } b.WriteString(fmt.Sprintf("%svisited=%v, key=%v, val=%v\n", h, n.visited.Load(), n.key, n.val)) } s.mu.Unlock() return b.String() } // -- internal methods -- // add a new tuple to the cache and evict as necessary // caller must hold lock. func (s *Sieve[K, V]) add(key K, val V) { // cache miss; we evict and fnd a new node if s.size == s.capacity { s.evict() } n := s.newNode(key, val) // Eviction is guaranteed to remove one node; so this should never happen. if n == nil { msg := fmt.Sprintf("%T: add <%v>: objpool empty after eviction", s, key) panic(msg) } s.cache.Put(key, n) // insert at the head of the list n.next = s.head n.prev = nil if s.head != nil { s.head.prev = n } s.head = n if s.tail == nil { s.tail = n } s.size += 1 } // evict an item from the cache. // NB: Caller must hold the lock func (s *Sieve[K, V]) evict() { hand := s.hand if hand == nil { hand = s.tail } for hand != nil { if !hand.visited.Load() { s.cache.Del(hand.key) s.remove(hand) s.hand = hand.prev return } hand.visited.Store(false) hand = hand.prev // wrap around and start again if hand == nil { hand = s.tail } } s.hand = hand } func (s *Sieve[K, V]) remove(n *node[K, V]) { s.size -= 1 // remove node from list if n.prev != nil { n.prev.next = n.next } else { s.head = n.next } if n.next != nil { n.next.prev = n.prev } else { s.tail = n.prev } s.pool.Put(n) } func (s *Sieve[K, V]) newNode(key K, val V) *node[K, V] { n := s.pool.Get() n.key, n.val = key, val n.next, n.prev = nil, nil n.visited.Store(false) return n } // desc describes the properties of the sieve func (s *Sieve[K, V]) desc() string { m := fmt.Sprintf("cache<%T>: size %d, cap %d, head=%p, tail=%p, hand=%p", s, s.size, s.capacity, s.head, s.tail, s.hand) return m } // Generic sync.Pool type syncPool[T any] struct { pool sync.Pool } func newSyncPool[T any]() *syncPool[T] { p := &syncPool[T]{ pool: sync.Pool{ New: func() any { return new(T) }, }, } return p } func (s *syncPool[T]) Get() *T { p := s.pool.Get() return p.(*T) } func (s *syncPool[T]) Put(n *T) { s.pool.Put(n) } // generic sync.Map type syncMap[K comparable, V any] struct { m sync.Map } func newSyncMap[K comparable, V any]() *syncMap[K, V] { m := syncMap[K, V]{} return &m } func (m *syncMap[K, V]) Get(key K) (V, bool) { v, ok := m.m.Load(key) if ok { return v.(V), true } var z V return z, false } func (m *syncMap[K, V]) Put(key K, val V) { m.m.Store(key, val) } func (m *syncMap[K, V]) Del(key K) (V, bool) { x, ok := m.m.LoadAndDelete(key) if ok { return x.(V), true } var z V return z, false }