diff --git a/clock/clock.go b/clock/clock.go index cb06d0b7..64c8d170 100644 --- a/clock/clock.go +++ b/clock/clock.go @@ -75,19 +75,18 @@ func (c *Cache[K, V]) Add(key K, val V) { value: val, }) c.indices[key] = len(c.buf) - 1 - c.clockhand = (c.clockhand + 1) % len(c.buf) - return - } - // Full, evict by reference bit then replace - hand := c.clockhand - for c.touches[hand].Load() > 0 { - c.touches[hand].Add(-1) - hand = (hand + 1) % len(c.buf) + } else { + // Full, evict by reference bit then replace + for c.touches[c.clockhand].Load() > 0 { + c.touches[c.clockhand].Add(-1) + c.clockhand += 1 % len(c.buf) + } + c.Evict(c.buf[c.clockhand].key) + c.buf.insertAt(c.clockhand, key, val) + c.indices[key] = c.clockhand + c.touches[c.clockhand].Add(1) } - c.Evict(c.buf[hand].key) - c.buf.insertAt(hand, key, val) - c.indices[key] = hand - c.clockhand = hand + c.clockhand = (c.clockhand + 1) % len(c.buf) } // Get returns the value for a given key, if present. diff --git a/clock/clock_test.go b/clock/clock_test.go index 9a6909b5..f3fc4f72 100644 --- a/clock/clock_test.go +++ b/clock/clock_test.go @@ -44,3 +44,96 @@ func TestCacheGet(t *testing.T) { }) } } + +func TestCacheAdd(t *testing.T) { + tests := []struct { + name string + size int + keysToAdd []string + valueToAdd []string + expectedCache buffer[string, string] + }{ + { + "beyond_capacity_evicts_first_untouched", + 3, + []string{"key-a", "key-b", "key-c", "key-d", "key-e"}, + []string{"val-a", "val-b", "val-c", "val-d", "val-e"}, + buffer[string, string]{ + &bufferItem[string, string]{key: "key-e", value: "val-e"}, + &bufferItem[string, string]{key: "key-b", value: "val-b"}, + &bufferItem[string, string]{key: "key-d", value: "val-d"}, + }, + }, + { + "multiple_touches_decreases_eviction_chances", + 3, + []string{"key-a", "key-b", "key-c", "key-a", "key-a", "key-d", "key-e"}, + []string{"val-a", "val-b", "val-c", "val-a", "val-a", "val-d", "val-e"}, + buffer[string, string]{ + &bufferItem[string, string]{key: "key-a", value: "val-a"}, + &bufferItem[string, string]{key: "key-e", value: "val-e"}, + &bufferItem[string, string]{key: "key-d", value: "val-d"}, + }, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + cache := New[string, string](test.size) + for i := 0; i < len(test.keysToAdd); i++ { + key := test.keysToAdd[i] + val := test.valueToAdd[i] + cache.Add(key, val) + } + for i := 0; i < len(cache.buf); i++ { + cachedKey := cache.buf[i].key + cachedValue := cache.buf[i].value + expectedKey := test.expectedCache[i].key + expectedValue := test.expectedCache[i].value + if cachedKey != expectedKey { + t.Fatalf("bad cache key; got %s, wanted %s at index %d", cachedKey, expectedKey, i) + } + if cachedValue != expectedValue { + t.Fatalf("bad cache value; got %s, wanted %s at index %d", cachedValue, expectedValue, i) + } + } + }) + } +} + +func BenchmarkGetAllHits(b *testing.B) { + b.ReportAllocs() + type complexStruct struct { + a, b, c, d, e, f int64 + k, l, m, n, o, p float64 + } + // Populate the cache + l := New[int, complexStruct](32) + for z := 0; z < 32; z++ { + l.Add(z, complexStruct{a: int64(z)}) + } + + b.ResetTimer() + for z := 0; z < b.N; z++ { + // take the lower 5 bits as mod 32 so we always hit + l.Get(z & 31) + } +} + +func BenchmarkGetHalfHits(b *testing.B) { + b.ReportAllocs() + type complexStruct struct { + a, b, c, d, e, f int64 + k, l, m, n, o, p float64 + } + // Populate the cache + l := New[int, complexStruct](32) + for z := 0; z < 32; z++ { + l.Add(z, complexStruct{a: int64(z)}) + } + + b.ResetTimer() + for z := 0; z < b.N; z++ { + // take the lower 4 bits as mod 16 shifted left by 1 to + l.Get((z&15)<<1 | z&16>>4 | z&1<<4) + } +}