blob: 5750f412bb0371dc71e4d43b8027e4fb2bdf48e1 [file] [log] [blame]
// Copyright 2023 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 file.
// The lru package implements a fixed-size in-memory LRU cache.
package lru
import (
"container/heap"
"fmt"
"sync"
)
type any = interface{} // TODO: remove once gopls only builds at go1.18+
// A Cache is a fixed-size in-memory LRU cache.
type Cache struct {
capacity int
mu sync.Mutex
used int // used capacity, in user-specified units
m map[any]*entry // k/v lookup
lru queue // min-atime priority queue of *entry
clock int64 // clock time, incremented whenever the cache is updated
}
type entry struct {
key any
value any
size int // caller-specified size
atime int64 // last access / set time
index int // index of entry in the heap slice
}
// New creates a new Cache with the given capacity, which must be positive.
//
// The cache capacity uses arbitrary units, which are specified during the Set
// operation.
func New(capacity int) *Cache {
if capacity == 0 {
panic("zero capacity")
}
return &Cache{
capacity: capacity,
m: make(map[any]*entry),
}
}
// Get retrieves the value for the specified key, or nil if the key is not
// found.
//
// If the key is found, its access time is updated.
func (c *Cache) Get(key any) any {
c.mu.Lock()
defer c.mu.Unlock()
c.clock++ // every access updates the clock
if e, ok := c.m[key]; ok { // cache hit
e.atime = c.clock
heap.Fix(&c.lru, e.index)
return e.value
}
return nil
}
// Set stores a value for the specified key, using its given size to update the
// current cache size, evicting old entries as necessary to fit in the cache
// capacity.
//
// Size must be a non-negative value. If size is larger than the cache
// capacity, the value is not stored and the cache is not modified.
func (c *Cache) Set(key, value any, size int) {
if size < 0 {
panic(fmt.Sprintf("size must be non-negative, got %d", size))
}
if size > c.capacity {
return // uncacheable
}
c.mu.Lock()
defer c.mu.Unlock()
c.clock++
// Remove the existing cache entry for key, if it exists.
e, ok := c.m[key]
if ok {
c.used -= e.size
heap.Remove(&c.lru, e.index)
delete(c.m, key)
}
// Evict entries until the new value will fit.
newUsed := c.used + size
if newUsed < 0 {
return // integer overflow; return silently
}
c.used = newUsed
for c.used > c.capacity {
// evict oldest entry
e = heap.Pop(&c.lru).(*entry)
c.used -= e.size
delete(c.m, e.key)
}
// Store the new value.
// Opt: e is evicted, so it can be reused to reduce allocation.
if e == nil {
e = new(entry)
}
e.key = key
e.value = value
e.size = size
e.atime = c.clock
c.m[e.key] = e
heap.Push(&c.lru, e)
if len(c.m) != len(c.lru) {
panic("map and LRU are inconsistent")
}
}
// -- priority queue boilerplate --
// queue is a min-atime priority queue of cache entries.
type queue []*entry
func (q queue) Len() int { return len(q) }
func (q queue) Less(i, j int) bool { return q[i].atime < q[j].atime }
func (q queue) Swap(i, j int) {
q[i], q[j] = q[j], q[i]
q[i].index = i
q[j].index = j
}
func (q *queue) Push(x any) {
e := x.(*entry)
e.index = len(*q)
*q = append(*q, e)
}
func (q *queue) Pop() any {
last := len(*q) - 1
e := (*q)[last]
(*q)[last] = nil // aid GC
*q = (*q)[:last]
return e
}