@@ -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.
526542func (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.
587606func (c * s3fifo [K , V ]) len () int {
588607 total := 0
0 commit comments