commit 511bdc61c597977bc668f5f34730c474731831cf Author: William Perron Date: Mon Jan 22 15:51:36 2024 -0500 initial commit Golang implementation of the [Sieve cache algorithm][1]. [1]: https://cachemon.github.io/SIEVE-website/blog/2023/12/17/sieve-is-simpler-than-lru/ diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..405e044 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module go.wperron.io/sieve + +go 1.21.0 diff --git a/sieve.go b/sieve.go new file mode 100644 index 0000000..43561c7 --- /dev/null +++ b/sieve.go @@ -0,0 +1,98 @@ +package sieve + +type node struct { + value string + visited bool + prev, next *node +} + +type SieveCache struct { + cap, size int + cache map[string]*node + head, tail, hand *node +} + +func NewCache(cap int) *SieveCache { + return &SieveCache{ + cap: cap, + size: 0, + cache: make(map[string]*node, cap), + } +} + +// Access touches the item `v` in the cache +func (sc *SieveCache) Access(v string) { + if n, ok := sc.cache[v]; ok { + n.visited = true + } else { + if sc.size == sc.cap { + sc.evict() + } + + newNode := &node{value: v} + sc.addToHead(newNode) + sc.cache[v] = newNode + sc.size += 1 + newNode.visited = false + } +} + +// Range iterates over the values in the cache, stops the iteration if the +// closure returns false. +func (sc *SieveCache) Range(f func(v string) bool) { + current := sc.head + keepGoing := true + for current != nil && keepGoing { + keepGoing = f(current.value) + current = current.next + } +} + +func (sc *SieveCache) evict() { + var obj *node + if sc.hand != nil { + obj = sc.hand + } else { + obj = sc.tail + } + + for obj != nil && obj.visited { + obj.visited = false + if obj.prev != nil { + obj = obj.prev + } else { + obj = sc.tail + } + } + + sc.hand = obj.prev + delete(sc.cache, obj.value) + sc.removeNode(obj) + sc.size -= 1 +} + +func (sc *SieveCache) removeNode(n *node) { + if n.prev != nil { + n.prev.next = n.next + } else { + sc.head = n.next + } + + if n.next != nil { + n.next.prev = n.prev + } else { + sc.tail = n.prev + } +} + +func (sc *SieveCache) addToHead(n *node) { + n.next = sc.head + n.prev = nil + if sc.head != nil { + sc.head.prev = n + } + sc.head = n + if sc.tail == nil { + sc.tail = n + } +} diff --git a/sieve_test.go b/sieve_test.go new file mode 100644 index 0000000..0e3503c --- /dev/null +++ b/sieve_test.go @@ -0,0 +1,18 @@ +package sieve + +import "testing" + +func TestSieveCache(t *testing.T) { + cache := NewCache(3) + cache.Access("A") + cache.Access("B") + cache.Access("C") + cache.Access("D") + cache.Access("B") + cache.Access("E") + cache.Access("A") + cache.Range(func(v string) bool { + t.Log(v) + return true + }) +}