Skip to content

Commit

Permalink
lru: Add TypedLRU
Browse files Browse the repository at this point in the history
Fork the existing LRU cache implementation and add implement a version
based on an LRU stack with strong-typing (using generics).

This should modestly improve the GC-friendliness, in that it will be
possible to use the lru package without forcing the GC to traverse any
pointers other than the linked-list in the LRU-stack.

Gate the new types on go 1.18+ with build tags so go 1.17 users can
still use the package.
  • Loading branch information
dfinkel committed Jul 28, 2022
1 parent ab82035 commit be0126c
Show file tree
Hide file tree
Showing 3 changed files with 336 additions and 0 deletions.
110 changes: 110 additions & 0 deletions lru/typed_ll.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
//go:build go1.18

/*
Copyright 2022 Vimeo Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package lru

// LinkedList using generics to reduce the number of heap objects
// Used for the LRU stack in TypedCache
type linkedList[T any] struct {
head *llElem[T]
tail *llElem[T]
size int
}

type llElem[T any] struct {
next, prev *llElem[T]
value T
}

func (l *llElem[T]) Next() *llElem[T] {
return l.next
}

func (l *llElem[T]) Prev() *llElem[T] {
return l.prev
}

func (l *linkedList[T]) PushFront(val T) *llElem[T] {
elem := llElem[T]{
next: l.head,
prev: nil, // first element
value: val,
}
if l.head != nil {
l.head.prev = &elem
}
if l.tail == nil {
l.tail = &elem
}
l.head = &elem
l.size++

return &elem
}

func (l *linkedList[T]) MoveToFront(e *llElem[T]) {
if l.head == e {
// nothing to do
return
}

if e.next != nil {
// update the previous pointer on the next element
e.next.prev = e.prev
}
if e.prev != nil {
e.prev.next = e.next
}
if l.head != nil {
l.head.prev = e
}

if l.tail == e {
l.tail = e.prev
}
}

func (l *linkedList[T]) Remove(e *llElem[T]) {
if l.tail == e {
l.tail = e.prev
}
if l.head == e {
l.head = e.next
}

if e.next != nil {
// update the previous pointer on the next element
e.next.prev = e.prev
}
if e.prev != nil {
e.prev.next = e.next
}
l.size--
}

func (l *linkedList[T]) Len() int {
return l.size
}

func (l *linkedList[T]) Front() *llElem[T] {
return l.head
}

func (l *linkedList[T]) Back() *llElem[T] {
return l.tail
}
141 changes: 141 additions & 0 deletions lru/typed_lru.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
//go:build go1.18

/*
Copyright 2013 Google Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

// Package lru implements an LRU cache.
package lru // import "github.com/vimeo/galaxycache/lru"

// TypedCache is an LRU cache. It is not safe for concurrent access.
type TypedCache[K comparable, V any] struct {
// MaxEntries is the maximum number of cache entries before
// an item is evicted. Zero means no limit.
MaxEntries int

// OnEvicted optionally specificies a callback function to be
// executed when an typedEntry is purged from the cache.
OnEvicted func(key K, value V)

ll linkedList[typedEntry[K, V]]
cache map[K]*llElem[typedEntry[K, V]]
}

type typedEntry[K comparable, V any] struct {
key K
value V
}

// TypedNew creates a new Cache (with types).
// If maxEntries is zero, the cache has no limit and it's assumed
// that eviction is done by the caller.
func TypedNew[K comparable, V any](maxEntries int) *TypedCache[K, V] {
return &TypedCache[K, V]{
MaxEntries: maxEntries,
cache: make(map[K]*llElem[typedEntry[K, V]]),
}
}

// Add adds a value to the cache.
func (c *TypedCache[K, V]) Add(key K, value V) {
if c.cache == nil {
c.cache = make(map[K]*llElem[typedEntry[K, V]])
}
if ele, hit := c.cache[key]; hit {
c.ll.MoveToFront(ele)
ele.value.value = value
return
}
ele := c.ll.PushFront(typedEntry[K, V]{key, value})
c.cache[key] = ele
if c.MaxEntries != 0 && c.ll.Len() > c.MaxEntries {
c.RemoveOldest()
}
}

// Get looks up a key's value from the cache.
func (c *TypedCache[K, V]) Get(key K) (value V, ok bool) {
if c.cache == nil {
return
}
if ele, hit := c.cache[key]; hit {
c.ll.MoveToFront(ele)
return ele.value.value, true
}
return
}

// MostRecent returns the most recently used element
func (c *TypedCache[K, V]) MostRecent() *V {
if c.Len() == 0 {
return nil
}
return &c.ll.Front().value.value
}

// LeastRecent returns the least recently used element
func (c *TypedCache[K, V]) LeastRecent() *V {
if c.Len() == 0 {
return nil
}
return &c.ll.Back().value.value
}

// Remove removes the provided key from the cache.
func (c *TypedCache[K, V]) Remove(key K) {
if c.cache == nil {
return
}
if ele, hit := c.cache[key]; hit {
c.removeElement(ele)
}
}

// RemoveOldest removes the oldest item from the cache.
func (c *TypedCache[K, V]) RemoveOldest() {
if c.cache == nil {
return
}
ele := c.ll.Back()
if ele != nil {
c.removeElement(ele)
}
}

func (c *TypedCache[K, V]) removeElement(e *llElem[typedEntry[K, V]]) {
c.ll.Remove(e)
kv := e.value
delete(c.cache, kv.key)
if c.OnEvicted != nil {
c.OnEvicted(kv.key, kv.value)
}
}

// Len returns the number of items in the cache.
func (c *TypedCache[K, V]) Len() int {
return c.ll.Len()
}

// Clear purges all stored items from the cache.
func (c *TypedCache[K, V]) Clear() {
if c.OnEvicted != nil {
for _, e := range c.cache {
kv := e.value
c.OnEvicted(kv.key, kv.value)
}
}
c.ll = linkedList[typedEntry[K, V]]{}
c.cache = nil
}
85 changes: 85 additions & 0 deletions lru/typed_lru_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
//go:build go1.18

/*
Copyright 2013 Google Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package lru

import (
"fmt"
"testing"
)

func TestTypedGet(t *testing.T) {
getTests := []struct {
name string
keyToAdd string
keyToGet string
expectedOk bool
}{
{"string_hit", "myKey", "myKey", true},
{"string_miss", "myKey", "nonsense", false},
}

for _, tt := range getTests {
lru := TypedNew[string, int](0)
lru.Add(tt.keyToAdd, 1234)
val, ok := lru.Get(tt.keyToGet)
if ok != tt.expectedOk {
t.Fatalf("%s: cache hit = %v; want %v", tt.name, ok, !ok)
} else if ok && val != 1234 {
t.Fatalf("%s expected get to return 1234 but got %v", tt.name, val)
}
}
}

func TestTypedRemove(t *testing.T) {
lru := TypedNew[string, int](0)
lru.Add("myKey", 1234)
if val, ok := lru.Get("myKey"); !ok {
t.Fatal("TestRemove returned no match")
} else if val != 1234 {
t.Fatalf("TestRemove failed. Expected %d, got %v", 1234, val)
}

lru.Remove("myKey")
if _, ok := lru.Get("myKey"); ok {
t.Fatal("TestRemove returned a removed entry")
}
}

func TestTypedEvict(t *testing.T) {
evictedKeys := make([]Key, 0)
onEvictedFun := func(key string, value int) {
evictedKeys = append(evictedKeys, key)
}

lru := TypedNew[string, int](20)
lru.OnEvicted = onEvictedFun
for i := 0; i < 22; i++ {
lru.Add(fmt.Sprintf("myKey%d", i), 1234)
}

if len(evictedKeys) != 2 {
t.Fatalf("got %d evicted keys; want 2", len(evictedKeys))
}
if evictedKeys[0] != Key("myKey0") {
t.Fatalf("got %v in first evicted key; want %s", evictedKeys[0], "myKey0")
}
if evictedKeys[1] != Key("myKey1") {
t.Fatalf("got %v in second evicted key; want %s", evictedKeys[1], "myKey1")
}
}

0 comments on commit be0126c

Please sign in to comment.