github.com/fufuok/freelru@v0.13.3/shardedlru.go (about) 1 package freelru 2 3 import ( 4 "errors" 5 "fmt" 6 "math/bits" 7 "runtime" 8 "sync" 9 "time" 10 ) 11 12 // ShardedLRU is a thread-safe, sharded, fixed size LRU cache. 13 // Sharding is used to reduce lock contention on high concurrency. 14 // The downside is that exact LRU behavior is not given (as for the LRU and SynchedLRU types). 15 type ShardedLRU[K comparable, V any] struct { 16 lrus []LRU[K, V] 17 mus []sync.RWMutex 18 hash HashKeyCallback[K] 19 shards uint32 20 mask uint32 21 } 22 23 var _ Cache[int, int] = (*ShardedLRU[int, int])(nil) 24 25 // SetLifetime sets the default lifetime of LRU elements. 26 // Lifetime 0 means "forever". 27 func (lru *ShardedLRU[K, V]) SetLifetime(lifetime time.Duration) { 28 for shard := range lru.lrus { 29 lru.mus[shard].Lock() 30 lru.lrus[shard].SetLifetime(lifetime) 31 lru.mus[shard].Unlock() 32 } 33 } 34 35 // SetOnEvict sets the OnEvict callback function. 36 // The onEvict function is called for each evicted lru entry. 37 func (lru *ShardedLRU[K, V]) SetOnEvict(onEvict OnEvictCallback[K, V]) { 38 for shard := range lru.lrus { 39 lru.mus[shard].Lock() 40 lru.lrus[shard].SetOnEvict(onEvict) 41 lru.mus[shard].Unlock() 42 } 43 } 44 45 func nextPowerOfTwo(val uint32) uint32 { 46 if bits.OnesCount32(val) != 1 { 47 return 1 << bits.Len32(val) 48 } 49 return val 50 } 51 52 // NewSharded creates a new thread-safe sharded LRU hashmap with the given capacity. 53 func NewSharded[K comparable, V any](capacity uint32, hash HashKeyCallback[K]) (*ShardedLRU[K, V], error) { 54 size := uint32(float64(capacity) * 1.25) // 25% extra space for fewer collisions 55 56 return NewShardedWithSize[K, V](uint32(runtime.GOMAXPROCS(0)*16), capacity, size, hash) 57 } 58 59 func NewShardedWithSize[K comparable, V any](shards, capacity, size uint32, hash HashKeyCallback[K]) ( 60 *ShardedLRU[K, V], error, 61 ) { 62 if capacity == 0 { 63 return nil, errors.New("capacity must be positive") 64 } 65 if size < capacity { 66 return nil, fmt.Errorf("size (%d) is smaller than capacity (%d)", size, capacity) 67 } 68 69 if size < 1<<31 { 70 size = nextPowerOfTwo(size) // next power of 2 so the LRUs can avoid costly divisions 71 } else { 72 size = 1 << 31 // the highest 2^N value that fits in a uint32 73 } 74 75 shards = nextPowerOfTwo(shards) // next power of 2 so we can avoid costly division for sharding 76 77 for shards > size/16 { 78 shards /= 16 79 } 80 if shards == 0 { 81 shards = 1 82 } 83 84 size /= shards // size per LRU 85 if size == 0 { 86 size = 1 87 } 88 89 capacity = (capacity + shards - 1) / shards // size per LRU 90 if capacity == 0 { 91 capacity = 1 92 } 93 94 lrus := make([]LRU[K, V], shards) 95 buckets := make([]uint32, size*shards) 96 elements := make([]element[K, V], size*shards) 97 98 from := 0 99 to := int(size) 100 for i := range lrus { 101 initLRU(&lrus[i], capacity, size, hash, buckets[from:to], elements[from:to]) 102 from = to 103 to += int(size) 104 } 105 106 return &ShardedLRU[K, V]{ 107 lrus: lrus, 108 mus: make([]sync.RWMutex, shards), 109 hash: hash, 110 shards: shards, 111 mask: shards - 1, 112 }, nil 113 } 114 115 // Len returns the number of elements stored in the cache. 116 func (lru *ShardedLRU[K, V]) Len() (length int) { 117 for shard := range lru.lrus { 118 lru.mus[shard].RLock() 119 length += lru.lrus[shard].Len() 120 lru.mus[shard].RUnlock() 121 } 122 return 123 } 124 125 // AddWithLifetime adds a key:value to the cache with a lifetime. 126 // Returns true, true if key was updated and eviction occurred. 127 func (lru *ShardedLRU[K, V]) AddWithLifetime(key K, value V, lifetime time.Duration) (evicted bool) { 128 hash := lru.hash(key) 129 shard := (hash >> 16) & lru.mask 130 131 lru.mus[shard].Lock() 132 evicted = lru.lrus[shard].addWithLifetime(hash, key, value, lifetime) 133 lru.mus[shard].Unlock() 134 135 return 136 } 137 138 // Add adds a key:value to the cache. 139 // Returns true, true if key was updated and eviction occurred. 140 func (lru *ShardedLRU[K, V]) Add(key K, value V) (evicted bool) { 141 hash := lru.hash(key) 142 shard := (hash >> 16) & lru.mask 143 144 lru.mus[shard].Lock() 145 evicted = lru.lrus[shard].add(hash, key, value) 146 lru.mus[shard].Unlock() 147 148 return 149 } 150 151 // Get looks up a key's value from the cache, setting it as the most 152 // recently used item. 153 func (lru *ShardedLRU[K, V]) Get(key K) (value V, ok bool) { 154 hash := lru.hash(key) 155 shard := (hash >> 16) & lru.mask 156 157 lru.mus[shard].Lock() 158 value, ok = lru.lrus[shard].get(hash, key) 159 lru.mus[shard].Unlock() 160 161 return 162 } 163 164 // Peek looks up a key's value from the cache, without changing its recent-ness. 165 func (lru *ShardedLRU[K, V]) Peek(key K) (value V, ok bool) { 166 hash := lru.hash(key) 167 shard := (hash >> 16) & lru.mask 168 169 lru.mus[shard].RLock() 170 value, ok = lru.lrus[shard].peek(hash, key) 171 lru.mus[shard].RUnlock() 172 173 return 174 } 175 176 // Contains checks for the existence of a key, without changing its recent-ness. 177 func (lru *ShardedLRU[K, V]) Contains(key K) (ok bool) { 178 hash := lru.hash(key) 179 shard := (hash >> 16) & lru.mask 180 181 lru.mus[shard].RLock() 182 ok = lru.lrus[shard].contains(hash, key) 183 lru.mus[shard].RUnlock() 184 185 return 186 } 187 188 // Remove removes the key from the cache. 189 // The return value indicates whether the key existed or not. 190 func (lru *ShardedLRU[K, V]) Remove(key K) (removed bool) { 191 hash := lru.hash(key) 192 shard := (hash >> 16) & lru.mask 193 194 lru.mus[shard].Lock() 195 removed = lru.lrus[shard].remove(hash, key) 196 lru.mus[shard].Unlock() 197 198 return 199 } 200 201 // Keys returns a slice of the keys in the cache, from oldest to newest. 202 func (lru *ShardedLRU[K, V]) Keys() []K { 203 keys := make([]K, 0, lru.shards*lru.lrus[0].cap) 204 for shard := range lru.lrus { 205 lru.mus[shard].RLock() 206 keys = append(keys, lru.lrus[shard].Keys()...) 207 lru.mus[shard].RUnlock() 208 } 209 210 return keys 211 } 212 213 // Purge purges all data (key and value) from the LRU. 214 func (lru *ShardedLRU[K, V]) Purge() { 215 for shard := range lru.lrus { 216 lru.mus[shard].Lock() 217 lru.lrus[shard].Purge() 218 lru.mus[shard].Unlock() 219 } 220 } 221 222 // Metrics returns the metrics of the cache. 223 func (lru *ShardedLRU[K, V]) Metrics() Metrics { 224 metrics := Metrics{} 225 226 for shard := range lru.lrus { 227 lru.mus[shard].Lock() 228 m := lru.lrus[shard].Metrics() 229 lru.mus[shard].Unlock() 230 231 addMetrics(&metrics, m) 232 } 233 234 return metrics 235 } 236 237 // ResetMetrics resets the metrics of the cache and returns the previous state. 238 func (lru *ShardedLRU[K, V]) ResetMetrics() Metrics { 239 metrics := Metrics{} 240 241 for shard := range lru.lrus { 242 lru.mus[shard].Lock() 243 m := lru.lrus[shard].ResetMetrics() 244 lru.mus[shard].Unlock() 245 246 addMetrics(&metrics, m) 247 } 248 249 return metrics 250 } 251 252 func addMetrics(dst *Metrics, src Metrics) { 253 dst.Inserts += src.Inserts 254 dst.Collisions += src.Collisions 255 dst.Evictions += src.Evictions 256 dst.Removals += src.Removals 257 dst.Hits += src.Hits 258 dst.Misses += src.Misses 259 dst.Capacity += src.Capacity 260 dst.Lifetime = src.Lifetime 261 dst.Len += src.Len 262 } 263 264 // just used for debugging 265 func (lru *ShardedLRU[K, V]) dump() { 266 for shard := range lru.lrus { 267 fmt.Printf("Shard %d:\n", shard) 268 lru.mus[shard].RLock() 269 lru.lrus[shard].dump() 270 lru.mus[shard].RUnlock() 271 fmt.Println("") 272 } 273 } 274 275 func (lru *ShardedLRU[K, V]) PrintStats() { 276 for shard := range lru.lrus { 277 fmt.Printf("Shard %d:\n", shard) 278 lru.mus[shard].RLock() 279 lru.lrus[shard].PrintStats() 280 lru.mus[shard].RUnlock() 281 fmt.Println("") 282 } 283 }