mirror of
https://github.com/luxfi/cache.git
synced 2026-09-24 00:24:18 +00:00
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:
@@ -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
|
||||
|
||||
@@ -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
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user