@@ -8,12 +8,15 @@ import (
88 "time"
99)
1010
11- const numShards = 32
11+ const (
12+ numShards = 32
13+ shardMask = numShards - 1 // For fast modulo via bitwise AND
14+ )
1215
1316// s3fifo implements the S3-FIFO eviction algorithm from SOSP'23 paper
1417// "FIFO queues are all you need for cache eviction"
1518//
16- // This implementation uses 16 -way sharding for improved concurrent performance.
19+ // This implementation uses 32 -way sharding for improved concurrent performance.
1720// Each shard is an independent S3-FIFO instance with its own queues and lock.
1821//
1922// Algorithm per shard:
@@ -38,7 +41,11 @@ type s3fifo[K comparable, V any] struct {
3841}
3942
4043// shard is an independent S3-FIFO cache partition.
44+ //
45+ //nolint:govet // fieldalignment: padding is intentional to prevent false sharing
4146type shard [K comparable , V any ] struct {
47+ mu sync.RWMutex
48+ _ [40 ]byte // Padding to prevent false sharing (mutex is 24 bytes, pad to 64-byte cache line)
4249 items map [K ]* entry [K , V ]
4350 small * list.List
4451 main * list.List
@@ -47,17 +54,16 @@ type shard[K comparable, V any] struct {
4754 capacity int
4855 smallCap int
4956 ghostCap int
50- mu sync.RWMutex
5157}
5258
5359// entry represents a cached item with metadata.
5460type entry [K comparable , V any ] struct {
55- expiry time. Time
56- key K
57- value V
58- element * list. Element
59- accessed atomic.Bool // Fast "was accessed" flag for S3-FIFO promotion
60- inSmall bool // True if in Small queue, false if in Main
61+ key K
62+ value V
63+ element * list. Element
64+ expiryNano int64 // Unix nanoseconds; 0 means no expiry
65+ accessed atomic.Bool // Fast "was accessed" flag for S3-FIFO promotion
66+ inSmall bool // True if in Small queue, false if in Main
6167}
6268
6369// newS3FIFO creates a new sharded S3-FIFO cache with the given total capacity.
@@ -109,35 +115,29 @@ func newShard[K comparable, V any](capacity int) *shard[K, V] {
109115}
110116
111117// getShard returns the shard for a given key using type-optimized hashing.
118+ // Uses bitwise AND with shardMask for fast modulo (numShards must be power of 2).
112119func (c * s3fifo [K , V ]) getShard (key K ) * shard [K , V ] {
113120 switch k := any (key ).(type ) {
114121 case int :
115122 if k < 0 {
116123 k = - k
117124 }
118- return c .shards [k % numShards ]
125+ return c .shards [k & shardMask ]
119126 case int64 :
120127 if k < 0 {
121128 k = - k
122129 }
123- return c .shards [k % numShards ]
130+ return c .shards [k & shardMask ]
124131 case uint :
125- return c .shards [k % numShards ]
132+ return c .shards [k & shardMask ]
126133 case uint64 :
127- return c .shards [k % numShards ]
134+ return c .shards [k & shardMask ]
128135 case string :
129- var h maphash.Hash
130- h .SetSeed (c .seed )
131- //nolint:gosec // G104: maphash.Hash.WriteString never returns error
132- h .WriteString (k )
133- return c .shards [h .Sum64 ()% numShards ]
136+ return c .shards [maphash .String (c .seed , k )& shardMask ]
134137 default :
135138 // Fallback for other types: convert to string and hash
136- var h maphash.Hash
137- h .SetSeed (c .seed )
138- //nolint:errcheck,gosec // maphash.Hash.WriteString never returns error
139- h .WriteString (any (key ).(string ))
140- return c .shards [h .Sum64 ()% numShards ]
139+ //nolint:errcheck,forcetypeassert // Only called for string-convertible types
140+ return c .shards [maphash .String (c .seed , any (key ).(string ))& shardMask ]
141141 }
142142}
143143
@@ -157,7 +157,8 @@ func (s *shard[K, V]) get(key K) (V, bool) {
157157 }
158158
159159 // Fast path: check expiration while holding read lock
160- if ! ent .expiry .IsZero () && time .Now ().After (ent .expiry ) {
160+ // Single int64 comparison; time.Now().UnixNano() only called if item has expiry
161+ if ent .expiryNano != 0 && time .Now ().UnixNano () > ent .expiryNano {
161162 s .mu .RUnlock ()
162163 var zero V
163164 return zero , false
@@ -175,18 +176,26 @@ func (s *shard[K, V]) get(key K) (V, bool) {
175176 return val , true
176177}
177178
179+ // timeToNano converts time.Time to Unix nanoseconds; zero time becomes 0.
180+ func timeToNano (t time.Time ) int64 {
181+ if t .IsZero () {
182+ return 0
183+ }
184+ return t .UnixNano ()
185+ }
186+
178187// setToMemory adds or updates a value in the in-memory cache.
179188func (c * s3fifo [K , V ]) setToMemory (key K , value V , expiry time.Time ) {
180- c .getShard (key ).set (key , value , expiry )
189+ c .getShard (key ).set (key , value , timeToNano ( expiry ) )
181190}
182191
183- func (s * shard [K , V ]) set (key K , value V , expiry time. Time ) {
192+ func (s * shard [K , V ]) set (key K , value V , expiryNano int64 ) {
184193 s .mu .Lock ()
185194
186195 // Update existing entry
187196 if ent , ok := s .items [key ]; ok {
188197 ent .value = value
189- ent .expiry = expiry
198+ ent .expiryNano = expiryNano
190199 s .mu .Unlock ()
191200 return
192201 }
@@ -201,10 +210,10 @@ func (s *shard[K, V]) set(key K, value V, expiry time.Time) {
201210
202211 // Create new entry
203212 ent := & entry [K , V ]{
204- key : key ,
205- value : value ,
206- expiry : expiry ,
207- inSmall : ! inGhost ,
213+ key : key ,
214+ value : value ,
215+ expiryNano : expiryNano ,
216+ inSmall : ! inGhost ,
208217 }
209218
210219 // Make room if at capacity
@@ -343,11 +352,11 @@ func (s *shard[K, V]) cleanup() int {
343352 s .mu .Lock ()
344353 defer s .mu .Unlock ()
345354
346- now := time .Now ()
355+ nowNano := time .Now (). UnixNano ()
347356 var expired []K
348357
349358 for key , ent := range s .items {
350- if ! ent .expiry . IsZero () && now . After ( ent .expiry ) {
359+ if ent .expiryNano != 0 && nowNano > ent .expiryNano {
351360 expired = append (expired , key )
352361 }
353362 }
@@ -420,6 +429,6 @@ func (c *s3fifo[K, V]) setExpiry(key K, expiry time.Time) {
420429 s .mu .Lock ()
421430 defer s .mu .Unlock ()
422431 if ent , ok := s .items [key ]; ok {
423- ent .expiry = expiry
432+ ent .expiryNano = timeToNano ( expiry )
424433 }
425434}
0 commit comments