Skip to content

Commit 61b1a8e

Browse files
Thomas StrombergThomas Stromberg
authored andcommitted
address s3fifo edge cases
1 parent 680b135 commit 61b1a8e

File tree

2 files changed

+357
-27
lines changed

2 files changed

+357
-27
lines changed

s3fifo.go

Lines changed: 46 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -181,10 +181,21 @@ func newS3FIFO[K comparable, V any](cfg *config) *s3fifo[K, V] {
181181
}
182182

183183
// More shards reduces lock contention but each shard needs enough
184-
// entries for S3-FIFO to work (32 min gives 3 small + 29 main).
185-
nshards := min(runtime.GOMAXPROCS(0)*8, capacity/32, maxShards)
186-
if nshards < 1 {
184+
// entries for S3-FIFO to work effectively.
185+
// For small caches, use single shard to match reference implementation behavior.
186+
var nshards int
187+
if capacity < 65536 {
188+
// Use single shard for caches < 64K to match reference s3-fifo
187189
nshards = 1
190+
} else {
191+
minShardSize := 256
192+
if capacity < 262144 {
193+
minShardSize = 2048 // Conservative for medium caches (< 256K)
194+
}
195+
nshards = min(runtime.GOMAXPROCS(0)*8, capacity/minShardSize, maxShards)
196+
if nshards < 1 {
197+
nshards = 1
198+
}
188199
}
189200
// Round to power of 2 for fast modulo.
190201
//nolint:gosec // G115: nshards bounded by [1, maxShards]
@@ -212,10 +223,10 @@ func newS3FIFO[K comparable, V any](cfg *config) *s3fifo[K, V] {
212223
}
213224

214225
// S3-FIFO paper recommends small queue at 10% of total capacity.
215-
// Ghost queue at 19% provides optimal balance for varied workloads.
226+
// Ghost queue at 100% matches reference implementation for better hit rate.
216227
var smallRatio, ghostRatio float64
217228
smallRatio = 0.10
218-
ghostRatio = 0.19
229+
ghostRatio = 1.0
219230

220231
// Prepare hasher for Bloom filter
221232
var hasher func(K) uint64
@@ -284,7 +295,7 @@ func newShard[K comparable, V any](capacity int, smallRatio, ghostRatio float64,
284295
return s
285296
}
286297

287-
func (s *shard[K, V]) getEntry() *entry[K, V] {
298+
func (s *shard[K, V]) newEntry() *entry[K, V] {
288299
if s.freeEntries != nil {
289300
e := s.freeEntries
290301
s.freeEntries = e.next
@@ -372,7 +383,7 @@ func (c *s3fifo[K, V]) get(key K) (V, bool) {
372383
return zero, false
373384
}
374385

375-
if f := ent.freq.Load(); f < 63 {
386+
if f := ent.freq.Load(); f < 3 {
376387
ent.freq.Store(f + 1)
377388
}
378389
return val, true
@@ -396,7 +407,7 @@ func (c *s3fifo[K, V]) get(key K) (V, bool) {
396407
return zero, false
397408
}
398409

399-
if f := ent.freq.Load(); f < 63 {
410+
if f := ent.freq.Load(); f < 3 {
400411
ent.freq.Store(f + 1)
401412
}
402413
return val, true
@@ -426,7 +437,7 @@ func (s *shard[K, V]) get(key K) (V, bool) {
426437

427438
// S3-FIFO: Mark as accessed for lazy promotion.
428439
// Fast path: check if already at max freq
429-
if f := ent.freq.Load(); f < 63 {
440+
if f := ent.freq.Load(); f < 3 {
430441
ent.freq.Store(f + 1)
431442
}
432443

@@ -455,6 +466,10 @@ func (s *shard[K, V]) set(key K, value V, expiryNano int64) {
455466
if ent, ok := s.entries[key]; ok {
456467
ent.value = value
457468
ent.expiryNano = expiryNano
469+
// Increment frequency on update (like reference s3-fifo)
470+
if f := ent.freq.Load(); f < 3 {
471+
ent.freq.Store(f + 1)
472+
}
458473
s.mu.Unlock()
459474
return
460475
}
@@ -469,15 +484,16 @@ func (s *shard[K, V]) set(key K, value V, expiryNano int64) {
469484
}
470485

471486
// Create new entry
472-
ent := s.getEntry()
487+
ent := s.newEntry()
473488
ent.key = key
474489
ent.value = value
475490
ent.expiryNano = expiryNano
476491
ent.inSmall = !inGhost
477492

478493
// Evict when at capacity (no overflow buffer)
479494
for s.small.len+s.main.len >= s.capacity {
480-
if s.small.len >= s.smallCap {
495+
// Use > instead of >= to match reference implementation
496+
if s.small.len > s.capacity/10 {
481497
s.evictFromSmall()
482498
} else {
483499
s.evictFromMain()
@@ -524,6 +540,8 @@ func (s *shard[K, V]) delete(key K) {
524540
// Items accessed more than once (freq > 1) are promoted to Main,
525541
// items with freq <= 1 are evicted to ghost queue.
526542
func (s *shard[K, V]) evictFromSmall() {
543+
mainCap := (s.capacity * 9) / 10 // 90% for main queue
544+
527545
for s.small.len > 0 {
528546
ent := s.small.front()
529547
s.small.remove(ent)
@@ -532,7 +550,18 @@ func (s *shard[K, V]) evictFromSmall() {
532550
if ent.freq.Load() <= 1 {
533551
// Not accessed enough - evict and track in ghost
534552
delete(s.entries, ent.key)
535-
s.addToGhost(ent.key)
553+
554+
// Add to ghost queue using two rotating Bloom filters
555+
h := s.hasher(ent.key)
556+
if !s.ghostActive.Contains(h) {
557+
s.ghostActive.Add(h)
558+
}
559+
// Rotate filters when active is full (provides approximate FIFO)
560+
if s.ghostActive.entries >= s.ghostCap {
561+
s.ghostAging.Reset()
562+
s.ghostActive, s.ghostAging = s.ghostAging, s.ghostActive
563+
}
564+
536565
s.putEntry(ent)
537566
return
538567
}
@@ -542,6 +571,11 @@ func (s *shard[K, V]) evictFromSmall() {
542571
ent.freq.Store(0)
543572
ent.inSmall = false
544573
s.main.pushBack(ent)
574+
575+
// Cascade eviction if main queue exceeds capacity
576+
if s.main.len > mainCap {
577+
s.evictFromMain()
578+
}
545579
}
546580
}
547581

@@ -568,21 +602,6 @@ func (s *shard[K, V]) evictFromMain() {
568602
}
569603
}
570604

571-
// addToGhost adds a key to the ghost queue using two rotating Bloom filters.
572-
func (s *shard[K, V]) addToGhost(key K) {
573-
h := s.hasher(key)
574-
if !s.ghostActive.Contains(h) {
575-
s.ghostActive.Add(h)
576-
}
577-
578-
// Rotate filters when active is full (provides approximate FIFO)
579-
if s.ghostActive.entries >= s.ghostCap {
580-
// Reset aging filter and swap - aging becomes new active
581-
s.ghostAging.Reset()
582-
s.ghostActive, s.ghostAging = s.ghostAging, s.ghostActive
583-
}
584-
}
585-
586605
// len returns the total number of entries across all shards.
587606
func (c *s3fifo[K, V]) len() int {
588607
total := 0

0 commit comments

Comments
 (0)