forked from orijtech/groupcache
-
Notifications
You must be signed in to change notification settings - Fork 12
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
Showing
3 changed files
with
336 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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") | ||
} | ||
} |