Add SizedCache and use linked hashmap for LRU

Updates Cache to use linked.Hashmap from utils package for improved
performance. Adds SizedCache for byte-size bounded caching.
This commit is contained in:
2025-12-19 08:44:07 -08:00
parent db55166d12
commit f225c58999
4 changed files with 183 additions and 62 deletions
+4 -1
View File
@@ -2,7 +2,10 @@ module github.com/luxfi/cache
go 1.25
require github.com/luxfi/metric v1.4.5
require (
github.com/luxfi/metric v1.4.5
github.com/luxfi/utils v1.1.0
)
require (
github.com/beorn7/perks v1.0.1 // indirect
+2
View File
@@ -12,6 +12,8 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/luxfi/metric v1.4.5 h1:fD6gtpzB5ebGB06/m4K7e5+tOV5RiVHSZCV+Zb8ZDxk=
github.com/luxfi/metric v1.4.5/go.mod h1:AUQ7NSNz9WndAcr/SKnOkP7XSFFnBXOa+ihtJYfDaQY=
github.com/luxfi/utils v1.1.0 h1:ti7HvjNwJd4ILDMERJtOAWE9mF8l+zqDVkgWnF7Agic=
github.com/luxfi/utils v1.1.0/go.mod h1:ABqhBdGNig0CaDcnNYldv1byS0BTNi5IKPbJSPF1p98=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
+37 -61
View File
@@ -5,114 +5,90 @@
package lru
import (
"container/list"
"sync"
"github.com/luxfi/cache"
"github.com/luxfi/utils"
"github.com/luxfi/utils/linked"
)
var _ cache.Cacher[struct{}, struct{}] = (*Cache[struct{}, struct{}])(nil)
// entry is a cache entry.
type entry[K comparable, V any] struct {
key K
value V
}
// Cache is a thread-safe LRU cache.
// Cache is a key value store with bounded size. If the size is attempted to be
// exceeded, then an element is removed from the cache before the insertion is
// done, based on evicting the least recently used value.
type Cache[K comparable, V any] struct {
lock sync.Mutex
elements *linked.Hashmap[K, V]
size int
elements map[K]*list.Element
order *list.List
// onEvict is called with the key and value of an entry before eviction.
onEvict func(K, V)
}
// NewCache creates a new LRU cache with the specified size.
func NewCache[K comparable, V any](size int) *Cache[K, V] {
if size <= 0 {
size = 1
}
return NewCacheWithOnEvict(size, func(K, V) {})
}
// NewCacheWithOnEvict creates a new LRU cache with the given size and eviction callback.
func NewCacheWithOnEvict[K comparable, V any](size int, onEvict func(K, V)) *Cache[K, V] {
return &Cache[K, V]{
size: size,
elements: make(map[K]*list.Element),
order: list.New(),
elements: linked.NewHashmap[K, V](),
size: max(size, 1),
onEvict: onEvict,
}
}
// Put inserts an element into the cache.
func (c *Cache[K, V]) Put(key K, value V) {
c.lock.Lock()
defer c.lock.Unlock()
if elem, ok := c.elements[key]; ok {
// Update existing entry
elem.Value.(*entry[K, V]).value = value
c.order.MoveToFront(elem)
return
}
// Evict oldest if at capacity
if c.order.Len() >= c.size {
oldest := c.order.Back()
if oldest != nil {
c.removeElement(oldest)
if c.elements.Len() == c.size {
oldestKey, oldestVal, _ := c.elements.Oldest()
c.elements.Delete(oldestKey)
if c.onEvict != nil {
c.onEvict(oldestKey, oldestVal)
}
}
// Add new entry
e := &entry[K, V]{key: key, value: value}
elem := c.order.PushFront(e)
c.elements[key] = elem
c.elements.Put(key, value)
}
// Get returns the entry with the key, if it exists.
func (c *Cache[K, V]) Get(key K) (V, bool) {
c.lock.Lock()
defer c.lock.Unlock()
if elem, ok := c.elements[key]; ok {
c.order.MoveToFront(elem)
return elem.Value.(*entry[K, V]).value, true
val, ok := c.elements.Get(key)
if !ok {
return utils.Zero[V](), false
}
var zero V
return zero, false
c.elements.Put(key, val) // Mark [k] as MRU.
return val, true
}
// Evict removes the specified entry from the cache.
func (c *Cache[K, V]) Evict(key K) {
func (c *Cache[K, _]) Evict(key K) {
c.lock.Lock()
defer c.lock.Unlock()
if elem, ok := c.elements[key]; ok {
c.removeElement(elem)
}
c.elements.Delete(key)
}
// Flush removes all entries from the cache.
func (c *Cache[K, V]) Flush() {
func (c *Cache[_, _]) Flush() {
c.lock.Lock()
defer c.lock.Unlock()
c.elements = make(map[K]*list.Element)
c.order.Init()
c.elements.Clear()
}
// Len returns the number of elements in the cache.
func (c *Cache[K, V]) Len() int {
func (c *Cache[_, _]) Len() int {
c.lock.Lock()
defer c.lock.Unlock()
return c.order.Len()
return c.elements.Len()
}
// PortionFilled returns fraction of cache currently filled.
func (c *Cache[K, V]) PortionFilled() float64 {
func (c *Cache[_, _]) PortionFilled() float64 {
c.lock.Lock()
defer c.lock.Unlock()
return float64(c.order.Len()) / float64(c.size)
}
func (c *Cache[K, V]) removeElement(elem *list.Element) {
e := elem.Value.(*entry[K, V])
delete(c.elements, e.key)
c.order.Remove(elem)
return float64(c.elements.Len()) / float64(c.size)
}
+140
View File
@@ -0,0 +1,140 @@
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package lru
import (
"sync"
"github.com/luxfi/cache"
"github.com/luxfi/utils"
"github.com/luxfi/utils/linked"
)
var _ cache.Cacher[struct{}, any] = (*SizedCache[struct{}, any])(nil)
// sizedElement is used to store the element with its size, so we don't
// calculate the size multiple times.
//
// This ensures that any inconsistencies returned by the size function can not
// corrupt the cache.
type sizedElement[V any] struct {
value V
size int
}
// SizedCache is a key value store with bounded size. If the size is attempted
// to be exceeded, then elements are removed from the cache until the bound is
// honored, based on evicting the least recently used value.
type SizedCache[K comparable, V any] struct {
lock sync.Mutex
elements *linked.Hashmap[K, *sizedElement[V]]
maxSize int
currentSize int
size func(K, V) int
}
func NewSizedCache[K comparable, V any](maxSize int, size func(K, V) int) *SizedCache[K, V] {
return &SizedCache[K, V]{
elements: linked.NewHashmap[K, *sizedElement[V]](),
maxSize: maxSize,
size: size,
}
}
func (c *SizedCache[K, V]) Put(key K, value V) {
c.lock.Lock()
defer c.lock.Unlock()
c.put(key, value)
}
func (c *SizedCache[K, V]) Get(key K) (V, bool) {
c.lock.Lock()
defer c.lock.Unlock()
return c.get(key)
}
func (c *SizedCache[K, V]) Evict(key K) {
c.lock.Lock()
defer c.lock.Unlock()
c.evict(key)
}
func (c *SizedCache[K, V]) Flush() {
c.lock.Lock()
defer c.lock.Unlock()
c.flush()
}
func (c *SizedCache[_, _]) Len() int {
c.lock.Lock()
defer c.lock.Unlock()
return c.len()
}
func (c *SizedCache[_, _]) PortionFilled() float64 {
c.lock.Lock()
defer c.lock.Unlock()
return c.portionFilled()
}
func (c *SizedCache[K, V]) put(key K, value V) {
newEntrySize := c.size(key, value)
if newEntrySize > c.maxSize {
c.flush()
return
}
if oldElement, ok := c.elements.Get(key); ok {
c.currentSize -= oldElement.size
}
// Remove elements until the size of elements in the cache <= [c.maxSize].
for c.currentSize > c.maxSize-newEntrySize {
oldestKey, oldestElement, _ := c.elements.Oldest()
c.elements.Delete(oldestKey)
c.currentSize -= oldestElement.size
}
c.elements.Put(key, &sizedElement[V]{
value: value,
size: newEntrySize,
})
c.currentSize += newEntrySize
}
func (c *SizedCache[K, V]) get(key K) (V, bool) {
element, ok := c.elements.Get(key)
if !ok {
return utils.Zero[V](), false
}
c.elements.Put(key, element) // Mark [k] as MRU.
return element.value, true
}
func (c *SizedCache[K, _]) evict(key K) {
if element, ok := c.elements.Get(key); ok {
c.elements.Delete(key)
c.currentSize -= element.size
}
}
func (c *SizedCache[K, V]) flush() {
c.elements.Clear()
c.currentSize = 0
}
func (c *SizedCache[_, _]) len() int {
return c.elements.Len()
}
func (c *SizedCache[_, _]) portionFilled() float64 {
return float64(c.currentSize) / float64(c.maxSize)
}