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  }