Skip to content

Commit 5eb8462

Browse files
Thomas StrombergThomas Stromberg
authored andcommitted
tune ghost map sizes to 1.5
1 parent 824f94b commit 5eb8462

File tree

1 file changed

+25
-31
lines changed

1 file changed

+25
-31
lines changed

s3fifo.go

Lines changed: 25 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -95,15 +95,15 @@ type shard[K comparable, V any] struct {
9595
small entryList[K, V] // Intrusive list for small queue
9696
main entryList[K, V] // Intrusive list for main queue
9797

98-
// Two-map ghost: tracks evicted keys without linked list overhead.
99-
// On swap: clear aging map, swap pointers. Provides approximate FIFO.
100-
ghostActive map[K]struct{} // current generation ghost entries
101-
ghostAging map[K]struct{} // previous generation ghost entries
102-
ghostCount int // entries in active map
98+
// Two-map ghost: tracks recently evicted keys with zero false positives.
99+
// Two maps rotate to provide approximate FIFO without linked list overhead.
100+
// When ghostActive fills up, ghostAging is cleared and they swap roles.
101+
ghostActive map[K]struct{} // current generation
102+
ghostAging map[K]struct{} // previous generation (read-only until swap)
103+
ghostCap int // max entries before rotation
103104

104105
capacity int
105106
smallCap int
106-
ghostCap int
107107

108108
// Free list for reducing allocations
109109
freeEntries *entry[K, V]
@@ -218,14 +218,18 @@ func newS3FIFO[K comparable, V any](cfg *config) *s3fifo[K, V] {
218218

219219
// Auto-tune ratios based on capacity
220220
// Note: Two-map ghost tracks 2x ghostRatio total (both maps can be nearly full).
221-
// This is intentional - longer ghost history improves promotion decisions.
221+
// Ghost ratio sweep results (Meta trace):
222+
// 0.5x: 68.15% / 76.05%
223+
// 1.0x: 68.42% / 76.27%
224+
// 1.5x: 68.53% / 76.34% <- sweet spot (good hit rate, reasonable memory)
225+
// 2.0x: 68.57% / 76.39% <- diminishing returns
222226
var smallRatio, ghostRatio float64
223227
if capacity <= 16384 {
224228
smallRatio = 0.01 // 1% for small caches (Zipf-friendly)
225-
ghostRatio = 0.5 // 50% per map = ~100% total for small caches
229+
ghostRatio = 1.0 // 100% per map = ~200% total for small caches
226230
} else {
227231
smallRatio = 0.05 // 5% for large caches (Meta trace optimal)
228-
ghostRatio = 1.0 // 100% per map = ~200% total for large caches
232+
ghostRatio = 1.5 // 150% per map = ~300% total for large caches
229233
}
230234

231235
for i := range numShards {
@@ -243,7 +247,7 @@ func newShard[K comparable, V any](capacity int, smallRatio, ghostRatio float64)
243247
smallCap = 1
244248
}
245249

246-
// Ghost queue: recommended 100%
250+
// Ghost capacity: controls rotation frequency
247251
ghostCap := int(float64(capacity) * ghostRatio)
248252
if ghostCap < 1 {
249253
ghostCap = 1
@@ -377,13 +381,11 @@ func (s *shard[K, V]) set(key K, value V, expiryNano int64) {
377381

378382
// Slow path: insert new key (already holding lock)
379383

380-
// Check if key is in ghost (two-map lookup)
384+
// Check if key is in ghost (zero false positives)
381385
_, inGhost := s.ghostActive[key]
382386
if !inGhost {
383387
_, inGhost = s.ghostAging[key]
384388
}
385-
// Note: We don't remove from ghost on hit - the key will naturally age out.
386-
// This is acceptable since ghost is just a hint for promotion decisions.
387389

388390
// Create new entry
389391
ent := s.getEntry()
@@ -394,7 +396,11 @@ func (s *shard[K, V]) set(key K, value V, expiryNano int64) {
394396

395397
// Evict when at capacity (no overflow buffer)
396398
for s.small.len+s.main.len >= s.capacity {
397-
s.evict()
399+
if s.small.len >= s.smallCap {
400+
s.evictFromSmall()
401+
} else {
402+
s.evictFromMain()
403+
}
398404
}
399405

400406
// Add to appropriate queue
@@ -433,15 +439,6 @@ func (s *shard[K, V]) delete(key K) {
433439
s.putEntry(ent)
434440
}
435441

436-
// evict removes one entry according to S3-FIFO algorithm.
437-
func (s *shard[K, V]) evict() {
438-
if s.small.len >= s.smallCap {
439-
s.evictFromSmall()
440-
return
441-
}
442-
s.evictFromMain()
443-
}
444-
445442
// evictFromSmall evicts an entry from the small queue.
446443
func (s *shard[K, V]) evictFromSmall() {
447444
for s.small.len > 0 {
@@ -487,17 +484,15 @@ func (s *shard[K, V]) evictFromMain() {
487484
}
488485
}
489486

490-
// addToGhost adds a key to the ghost queue.
487+
// addToGhost adds a key to the ghost queue using two rotating maps.
491488
func (s *shard[K, V]) addToGhost(key K) {
492-
// Add to active generation
493489
s.ghostActive[key] = struct{}{}
494-
s.ghostCount++
495490

496-
// Swap generations when active is full (provides approximate FIFO)
497-
if s.ghostCount >= s.ghostCap {
491+
// Rotate maps when active is full (provides approximate FIFO)
492+
if len(s.ghostActive) >= s.ghostCap {
493+
// Clear aging map and swap - aging becomes new active
498494
clear(s.ghostAging)
499-
s.ghostAging, s.ghostActive = s.ghostActive, s.ghostAging
500-
s.ghostCount = 0
495+
s.ghostActive, s.ghostAging = s.ghostAging, s.ghostActive
501496
}
502497
}
503498

@@ -532,6 +527,5 @@ func (s *shard[K, V]) flush() int {
532527
s.main.init()
533528
clear(s.ghostActive)
534529
clear(s.ghostAging)
535-
s.ghostCount = 0
536530
return n
537531
}

0 commit comments

Comments
 (0)