@@ -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.
446443func (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 .
491488func (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