github.com/phuslu/lru@v1.0.16-0.20240421170520-46288a2fd47c/README.md (about)

     1  # a high-performance and gc-friendly LRU cache
     2  
     3  [![godoc][godoc-img]][godoc] [![release][release-img]][release] [![goreport][goreport-img]][goreport] [![codecov][codecov-img]][codecov]
     4  
     5  ### Features
     6  
     7  * Simple
     8      - No Dependencies.
     9      - Straightforward API.
    10  * Fast
    11      - Outperforms well-known *LRU* caches.
    12      - Zero memory allocations.
    13  * GC friendly
    14      - Pointerless and continuous data structs.
    15      - Minimized GC scan times.
    16  * Memory efficient
    17      - Adds only 26 extra bytes per entry.
    18      - Minimized memory usage.
    19  * Feature optional
    20      - Using SlidingCache via `WithSliding(true)` option.
    21      - Create LoadingCache via `WithLoader(func(context.Context, K) (V, time.Duration, error))` option.
    22  
    23  ### Limitations
    24  1. The TTL is accurate to the nearest second.
    25  2. Expired items are only removed when accessed again or the cache is full.
    26  
    27  ### Getting Started
    28  
    29  ```go
    30  package main
    31  
    32  import (
    33  	"time"
    34  
    35  	"github.com/phuslu/lru"
    36  )
    37  
    38  func main() {
    39  	cache := lru.NewTTLCache[string, int](8192)
    40  
    41  	cache.Set("a", 1, 2*time.Second)
    42  	println(cache.Get("a"))
    43  	println(cache.Get("b"))
    44  
    45  	time.Sleep(1 * time.Second)
    46  	println(cache.Get("a"))
    47  
    48  	time.Sleep(2 * time.Second)
    49  	println(cache.Get("a"))
    50  
    51  	stats := cache.Stats()
    52  	println("SetCalls", stats.SetCalls, "GetCalls", stats.GetCalls, "Misses", stats.Misses)
    53  }
    54  ```
    55  
    56  ### Throughput benchmarks
    57  
    58  *Disclaimer: This have been testing on the busy GitHub runners with 8 vCPUs and the results may be very different from your real environment. see https://github.com/phuslu/lru/issues/14*
    59  
    60  A Performance result as below. Check github [benchmark][benchmark] action for more results and details.
    61  <details>
    62    <summary>go1.22 benchmark on keysize=16, itemsize=1000000, cachesize=50%, concurrency=8</summary>
    63  
    64  ```go
    65  // env writeratio=0.1 zipfian=false go test -v -cpu=8 -run=none -bench=. -benchtime=5s -benchmem bench_test.go
    66  package bench
    67  
    68  import (
    69  	"crypto/sha1"
    70  	"fmt"
    71  	"math/rand/v2"
    72  	"math/bits"
    73  	"os"
    74  	"runtime"
    75  	"strconv"
    76  	"testing"
    77  	"time"
    78  	_ "unsafe"
    79  
    80  	theine "github.com/Yiling-J/theine-go"
    81  	"github.com/cespare/xxhash/v2"
    82  	cloudflare "github.com/cloudflare/golibs/lrucache"
    83  	ristretto "github.com/dgraph-io/ristretto"
    84  	freelru "github.com/elastic/go-freelru"
    85  	hashicorp "github.com/hashicorp/golang-lru/v2/expirable"
    86  	ccache "github.com/karlseguin/ccache/v3"
    87  	lxzan "github.com/lxzan/memorycache"
    88  	otter "github.com/maypok86/otter"
    89  	ecache "github.com/orca-zhang/ecache"
    90  	phuslu "github.com/phuslu/lru"
    91  	"github.com/aclements/go-perfevent/perfbench"
    92  )
    93  
    94  const (
    95  	keysize   = 16
    96  	cachesize = 1000000
    97  )
    98  
    99  var writeratio, _ = strconv.ParseFloat(os.Getenv("writeratio"), 64)
   100  var zipfian, _ = strconv.ParseBool(os.Getenv("zipfian"))
   101  
   102  type CheapRand struct {
   103  	Seed uint64
   104  }
   105  
   106  func (rand *CheapRand) Uint32() uint32 {
   107  	rand.Seed += 0xa0761d6478bd642f
   108  	hi, lo := bits.Mul64(rand.Seed, rand.Seed^0xe7037ed1a0b428db)
   109  	return uint32(hi ^ lo)
   110  }
   111  
   112  func (rand *CheapRand) Uint32n(n uint32) uint32 {
   113  	return uint32((uint64(rand.Uint32()) * uint64(n)) >> 32)
   114  }
   115  
   116  func (rand *CheapRand) Uint64() uint64 {
   117  	return uint64(rand.Uint32())<<32 ^ uint64(rand.Uint32())
   118  }
   119  
   120  var shardcount = func() int {
   121  	n := runtime.GOMAXPROCS(0) * 16
   122  	k := 1
   123  	for k < n {
   124  		k = k * 2
   125  	}
   126  	return k
   127  }()
   128  
   129  var keys = func() (x []string) {
   130  	x = make([]string, cachesize)
   131  	for i := range cachesize {
   132  		x[i] = fmt.Sprintf("%x", sha1.Sum([]byte(fmt.Sprint(i))))[:keysize]
   133  	}
   134  	return
   135  }()
   136  
   137  func BenchmarkHashicorpSetGet(b *testing.B) {
   138  	c := perfbench.Open(b)
   139  	cache := hashicorp.NewLRU[string, int](cachesize, nil, time.Hour)
   140  	for i := range cachesize/2 {
   141  		cache.Add(keys[i], i)
   142  	}
   143  
   144  	b.ResetTimer()
   145  	c.Reset()
   146  	b.RunParallel(func(pb *testing.PB) {
   147  		threshold := uint32(float64(^uint32(0)) * writeratio)
   148  		cheaprand := &CheapRand{uint64(time.Now().UnixNano())}
   149  		zipf := rand.NewZipf(rand.New(cheaprand), 1.0001, 10, cachesize-1)
   150  		for pb.Next() {
   151  			if threshold > 0 && cheaprand.Uint32() <= threshold {
   152  				i := int(cheaprand.Uint32n(cachesize))
   153  				cache.Add(keys[i], i)
   154  			} else if zipfian {
   155  				cache.Get(keys[zipf.Uint64()])
   156  			} else {
   157  				cache.Get(keys[cheaprand.Uint32n(cachesize)])
   158  			}
   159  		}
   160  	})
   161  }
   162  
   163  func BenchmarkCloudflareSetGet(b *testing.B) {
   164  	c := perfbench.Open(b)
   165  	cache := cloudflare.NewMultiLRUCache(uint(shardcount), uint(cachesize/shardcount))
   166  	for i := range cachesize/2 {
   167  		cache.Set(keys[i], i, time.Now().Add(time.Hour))
   168  	}
   169  	expires := time.Now().Add(time.Hour)
   170  
   171  	b.ResetTimer()
   172  	c.Reset()
   173  	b.RunParallel(func(pb *testing.PB) {
   174  		threshold := uint32(float64(^uint32(0)) * writeratio)
   175  		cheaprand := &CheapRand{uint64(time.Now().UnixNano())}
   176  		zipf := rand.NewZipf(rand.New(cheaprand), 1.0001, 10, cachesize-1)
   177  		for pb.Next() {
   178  			if threshold > 0 && cheaprand.Uint32() <= threshold {
   179  				i := int(cheaprand.Uint32n(cachesize))
   180  				cache.Set(keys[i], i, expires)
   181  			} else if zipfian {
   182  				cache.Get(keys[zipf.Uint64()])
   183  			} else {
   184  				cache.Get(keys[cheaprand.Uint32n(cachesize)])
   185  			}
   186  		}
   187  	})
   188  }
   189  
   190  func BenchmarkEcacheSetGet(b *testing.B) {
   191  	c := perfbench.Open(b)
   192  	cache := ecache.NewLRUCache(uint16(shardcount), uint16(cachesize/shardcount), time.Hour)
   193  	for i := range cachesize/2 {
   194  		cache.Put(keys[i], i)
   195  	}
   196  
   197  	b.ResetTimer()
   198  	c.Reset()
   199  	b.RunParallel(func(pb *testing.PB) {
   200  		threshold := uint32(float64(^uint32(0)) * writeratio)
   201  		cheaprand := &CheapRand{uint64(time.Now().UnixNano())}
   202  		zipf := rand.NewZipf(rand.New(cheaprand), 1.0001, 10, cachesize-1)
   203  		for pb.Next() {
   204  			if threshold > 0 && cheaprand.Uint32() <= threshold {
   205  				i := int(cheaprand.Uint32n(cachesize))
   206  				cache.Put(keys[i], i)
   207  			} else if zipfian {
   208  				cache.Get(keys[zipf.Uint64()])
   209  			} else {
   210  				cache.Get(keys[cheaprand.Uint32n(cachesize)])
   211  			}
   212  		}
   213  	})
   214  }
   215  
   216  func BenchmarkLxzanSetGet(b *testing.B) {
   217  	c := perfbench.Open(b)
   218  	cache := lxzan.New[string, int](
   219  		lxzan.WithBucketNum(shardcount),
   220  		lxzan.WithBucketSize(cachesize/shardcount, cachesize/shardcount),
   221  		lxzan.WithInterval(time.Hour, time.Hour),
   222  	)
   223  	for i := range cachesize/2 {
   224  		cache.Set(keys[i], i, time.Hour)
   225  	}
   226  
   227  	b.ResetTimer()
   228  	c.Reset()
   229  	b.RunParallel(func(pb *testing.PB) {
   230  		threshold := uint32(float64(^uint32(0)) * writeratio)
   231  		cheaprand := &CheapRand{uint64(time.Now().UnixNano())}
   232  		zipf := rand.NewZipf(rand.New(cheaprand), 1.0001, 10, cachesize-1)
   233  		for pb.Next() {
   234  			if threshold > 0 && cheaprand.Uint32() <= threshold {
   235  				i := int(cheaprand.Uint32n(cachesize))
   236  				cache.Set(keys[i], i, time.Hour)
   237  			} else if zipfian {
   238  				cache.Get(keys[zipf.Uint64()])
   239  			} else {
   240  				cache.Get(keys[cheaprand.Uint32n(cachesize)])
   241  			}
   242  		}
   243  	})
   244  }
   245  
   246  func hashStringXXHASH(s string) uint32 {
   247  	return uint32(xxhash.Sum64String(s))
   248  }
   249  
   250  func BenchmarkFreelruSetGet(b *testing.B) {
   251  	c := perfbench.Open(b)
   252  	cache, _ := freelru.NewSharded[string, int](cachesize, hashStringXXHASH)
   253  	for i := range cachesize/2 {
   254  		cache.AddWithLifetime(keys[i], i, time.Hour)
   255  	}
   256  
   257  	b.ResetTimer()
   258  	c.Reset()
   259  	b.RunParallel(func(pb *testing.PB) {
   260  		threshold := uint32(float64(^uint32(0)) * writeratio)
   261  		cheaprand := &CheapRand{uint64(time.Now().UnixNano())}
   262  		zipf := rand.NewZipf(rand.New(cheaprand), 1.0001, 10, cachesize-1)
   263  		for pb.Next() {
   264  			if threshold > 0 && cheaprand.Uint32() <= threshold {
   265  				i := int(cheaprand.Uint32n(cachesize))
   266  				cache.AddWithLifetime(keys[i], i, time.Hour)
   267  			} else if zipfian {
   268  				cache.Get(keys[zipf.Uint64()])
   269  			} else {
   270  				cache.Get(keys[cheaprand.Uint32n(cachesize)])
   271  			}
   272  		}
   273  	})
   274  }
   275  
   276  func BenchmarkPhusluSetGet(b *testing.B) {
   277  	c := perfbench.Open(b)
   278  	cache := phuslu.NewTTLCache[string, int](cachesize, phuslu.WithShards[string, int](uint32(shardcount)))
   279  	for i := range cachesize/2 {
   280  		cache.Set(keys[i], i, time.Hour)
   281  	}
   282  
   283  	b.ResetTimer()
   284  	c.Reset()
   285  	b.RunParallel(func(pb *testing.PB) {
   286  		threshold := uint32(float64(^uint32(0)) * writeratio)
   287  		cheaprand := &CheapRand{uint64(time.Now().UnixNano())}
   288  		zipf := rand.NewZipf(rand.New(cheaprand), 1.0001, 10, cachesize-1)
   289  		for pb.Next() {
   290  			if threshold > 0 && cheaprand.Uint32() <= threshold {
   291  				i := int(cheaprand.Uint32n(cachesize))
   292  				cache.Set(keys[i], i, time.Hour)
   293  			} else if zipfian {
   294  				cache.Get(keys[zipf.Uint64()])
   295  			} else {
   296  				cache.Get(keys[cheaprand.Uint32n(cachesize)])
   297  			}
   298  		}
   299  	})
   300  }
   301  
   302  func BenchmarkNoTTLSetGet(b *testing.B) {
   303  	c := perfbench.Open(b)
   304  	cache := phuslu.NewLRUCache[string, int](cachesize, phuslu.WithShards[string, int](uint32(shardcount)))
   305  	for i := range cachesize/2 {
   306  		cache.Set(keys[i], i)
   307  	}
   308  
   309  	b.ResetTimer()
   310  	c.Reset()
   311  	b.RunParallel(func(pb *testing.PB) {
   312  		threshold := uint32(float64(^uint32(0)) * writeratio)
   313  		cheaprand := &CheapRand{uint64(time.Now().UnixNano())}
   314  		zipf := rand.NewZipf(rand.New(cheaprand), 1.0001, 10, cachesize-1)
   315  		for pb.Next() {
   316  			if threshold > 0 && cheaprand.Uint32() <= threshold {
   317  				i := int(cheaprand.Uint32n(cachesize))
   318  				cache.Set(keys[i], i)
   319  			} else if zipfian {
   320  				cache.Get(keys[zipf.Uint64()])
   321  			} else {
   322  				cache.Get(keys[cheaprand.Uint32n(cachesize)])
   323  			}
   324  		}
   325  	})
   326  }
   327  
   328  func BenchmarkCcacheSetGet(b *testing.B) {
   329  	c := perfbench.Open(b)
   330  	cache := ccache.New(ccache.Configure[int]().MaxSize(cachesize).ItemsToPrune(100))
   331  	for i := range cachesize/2 {
   332  		cache.Set(keys[i], i, time.Hour)
   333  	}
   334  
   335  	b.ResetTimer()
   336  	c.Reset()
   337  	b.RunParallel(func(pb *testing.PB) {
   338  		threshold := uint32(float64(^uint32(0)) * writeratio)
   339  		cheaprand := &CheapRand{uint64(time.Now().UnixNano())}
   340  		zipf := rand.NewZipf(rand.New(cheaprand), 1.0001, 10, cachesize-1)
   341  		for pb.Next() {
   342  			if threshold > 0 && cheaprand.Uint32() <= threshold {
   343  				i := int(cheaprand.Uint32n(cachesize))
   344  				cache.Set(keys[i], i, time.Hour)
   345  			} else if zipfian {
   346  				cache.Get(keys[zipf.Uint64()])
   347  			} else {
   348  				cache.Get(keys[cheaprand.Uint32n(cachesize)])
   349  			}
   350  		}
   351  	})
   352  }
   353  
   354  func BenchmarkRistrettoSetGet(b *testing.B) {
   355  	c := perfbench.Open(b)
   356  	cache, _ := ristretto.NewCache(&ristretto.Config{
   357  		NumCounters: 10 * cachesize, // number of keys to track frequency of (10M).
   358  		MaxCost:     cachesize,      // maximum cost of cache (1M).
   359  		BufferItems: 64,             // number of keys per Get buffer.
   360  	})
   361  	for i := range cachesize/2 {
   362  		cache.SetWithTTL(keys[i], i, 1, time.Hour)
   363  	}
   364  
   365  	b.ResetTimer()
   366  	c.Reset()
   367  	b.RunParallel(func(pb *testing.PB) {
   368  		threshold := uint32(float64(^uint32(0)) * writeratio)
   369  		cheaprand := &CheapRand{uint64(time.Now().UnixNano())}
   370  		zipf := rand.NewZipf(rand.New(cheaprand), 1.0001, 10, cachesize-1)
   371  		for pb.Next() {
   372  			if threshold > 0 && cheaprand.Uint32() <= threshold {
   373  				i := int(cheaprand.Uint32n(cachesize))
   374  				cache.SetWithTTL(keys[i], i, 1, time.Hour)
   375  			} else if zipfian {
   376  				cache.Get(keys[zipf.Uint64()])
   377  			} else {
   378  				cache.Get(keys[cheaprand.Uint32n(cachesize)])
   379  			}
   380  		}
   381  	})
   382  }
   383  
   384  func BenchmarkTheineSetGet(b *testing.B) {
   385  	c := perfbench.Open(b)
   386  	cache, _ := theine.NewBuilder[string, int](cachesize).Build()
   387  	for i := range cachesize/2 {
   388  		cache.SetWithTTL(keys[i], i, 1, time.Hour)
   389  	}
   390  
   391  	b.ResetTimer()
   392  	c.Reset()
   393  	b.RunParallel(func(pb *testing.PB) {
   394  		threshold := uint32(float64(^uint32(0)) * writeratio)
   395  		cheaprand := &CheapRand{uint64(time.Now().UnixNano())}
   396  		zipf := rand.NewZipf(rand.New(cheaprand), 1.0001, 10, cachesize-1)
   397  		for pb.Next() {
   398  			if threshold > 0 && cheaprand.Uint32() <= threshold {
   399  				i := int(cheaprand.Uint32n(cachesize))
   400  				cache.SetWithTTL(keys[i], i, 1, time.Hour)
   401  			} else if zipfian {
   402  				cache.Get(keys[zipf.Uint64()])
   403  			} else {
   404  				cache.Get(keys[cheaprand.Uint32n(cachesize)])
   405  			}
   406  		}
   407  	})
   408  }
   409  
   410  func BenchmarkOtterSetGet(b *testing.B) {
   411  	c := perfbench.Open(b)
   412  	cache, _ := otter.MustBuilder[string, int](cachesize).WithVariableTTL().Build()
   413  	for i := range cachesize/2 {
   414  		cache.Set(keys[i], i, time.Hour)
   415  	}
   416  
   417  	b.ResetTimer()
   418  	c.Reset()
   419  	b.RunParallel(func(pb *testing.PB) {
   420  		threshold := uint32(float64(^uint32(0)) * writeratio)
   421  		cheaprand := &CheapRand{uint64(time.Now().UnixNano())}
   422  		zipf := rand.NewZipf(rand.New(cheaprand), 1.0001, 10, cachesize-1)
   423  		for pb.Next() {
   424  			if threshold > 0 && cheaprand.Uint32() <= threshold {
   425  				i := int(cheaprand.Uint32n(cachesize))
   426  				cache.Set(keys[i], i, time.Hour)
   427  			} else if zipfian {
   428  				cache.Get(keys[zipf.Uint64()])
   429  			} else {
   430  				cache.Get(keys[cheaprand.Uint32n(cachesize)])
   431  			}
   432  		}
   433  	})
   434  }
   435  ```
   436  </details>
   437  
   438  with randomly read (90%) and randomly write(10%)
   439  ```
   440  goos: linux
   441  goarch: amd64
   442  cpu: AMD EPYC 7763 64-Core Processor                
   443  BenchmarkHashicorpSetGet
   444  BenchmarkHashicorpSetGet-8    	13146066	       553.3 ns/op	      11 B/op	       0 allocs/op
   445  BenchmarkCloudflareSetGet
   446  BenchmarkCloudflareSetGet-8   	36612316	       208.6 ns/op	      16 B/op	       1 allocs/op
   447  BenchmarkEcacheSetGet
   448  BenchmarkEcacheSetGet-8       	47435138	       138.0 ns/op	       2 B/op	       0 allocs/op
   449  BenchmarkLxzanSetGet
   450  BenchmarkLxzanSetGet-8        	48343774	       153.5 ns/op	       0 B/op	       0 allocs/op
   451  BenchmarkFreelruSetGet
   452  BenchmarkFreelruSetGet-8      	56105211	       137.8 ns/op	       0 B/op	       0 allocs/op
   453  BenchmarkPhusluSetGet
   454  BenchmarkPhusluSetGet-8       	66522236	       114.7 ns/op	       0 B/op	       0 allocs/op
   455  BenchmarkCcacheSetGet
   456  BenchmarkCcacheSetGet-8       	21252092	       369.4 ns/op	      34 B/op	       2 allocs/op
   457  BenchmarkRistrettoSetGet
   458  BenchmarkRistrettoSetGet-8    	35511078	       152.7 ns/op	      29 B/op	       1 allocs/op
   459  BenchmarkTheineSetGet
   460  BenchmarkTheineSetGet-8       	21374548	       311.4 ns/op	       5 B/op	       0 allocs/op
   461  BenchmarkOtterSetGet
   462  BenchmarkOtterSetGet-8        	51896601	       159.1 ns/op	       8 B/op	       0 allocs/op
   463  PASS
   464  ok  	command-line-arguments	113.829s
   465  ```
   466  
   467  with zipfian read (99%) and randomly write(1%)
   468  ```
   469  goos: linux
   470  goarch: amd64
   471  cpu: AMD EPYC 7763 64-Core Processor                
   472  BenchmarkHashicorpSetGet
   473  BenchmarkHashicorpSetGet-8    	14631464	       418.5 ns/op	       0 B/op	       0 allocs/op
   474  BenchmarkCloudflareSetGet
   475  BenchmarkCloudflareSetGet-8   	48996306	       129.3 ns/op	      16 B/op	       1 allocs/op
   476  BenchmarkEcacheSetGet
   477  BenchmarkEcacheSetGet-8       	61667361	       101.6 ns/op	       0 B/op	       0 allocs/op
   478  BenchmarkLxzanSetGet
   479  BenchmarkLxzanSetGet-8        	59331700	        99.66 ns/op	       0 B/op	       0 allocs/op
   480  BenchmarkFreelruSetGet
   481  BenchmarkFreelruSetGet-8      	57392088	       113.7 ns/op	       0 B/op	       0 allocs/op
   482  BenchmarkPhusluSetGet
   483  BenchmarkPhusluSetGet-8       	78875428	        81.73 ns/op	       0 B/op	       0 allocs/op
   484  BenchmarkCcacheSetGet
   485  BenchmarkCcacheSetGet-8       	23366601	       270.9 ns/op	      21 B/op	       2 allocs/op
   486  BenchmarkRistrettoSetGet
   487  BenchmarkRistrettoSetGet-8    	44893608	       114.3 ns/op	      20 B/op	       1 allocs/op
   488  BenchmarkTheineSetGet
   489  BenchmarkTheineSetGet-8       	32717158	       172.2 ns/op	       0 B/op	       0 allocs/op
   490  BenchmarkOtterSetGet
   491  BenchmarkOtterSetGet-8        	70170547	        85.62 ns/op	       1 B/op	       0 allocs/op
   492  PASS
   493  ok  	command-line-arguments	96.989s
   494  ```
   495  
   496  ### GC scan
   497  
   498  The GC scan times as below. Check github [gcscan][gcscan] action for more results and details.
   499  <details>
   500    <summary>GC scan times on keysize=16(string), valuesize=8(int), cachesize in (100000,200000,400000,1000000)</summary>
   501  
   502  ```go
   503  // env GODEBUG=gctrace=1 go run gcscan.go phuslu 1000000 
   504  package main
   505  
   506  import (
   507  	"fmt"
   508  	"os"
   509  	"runtime"
   510  	"runtime/debug"
   511  	"strconv"
   512  	"time"
   513  
   514  	theine "github.com/Yiling-J/theine-go"
   515  	"github.com/cespare/xxhash/v2"
   516  	cloudflare "github.com/cloudflare/golibs/lrucache"
   517  	ristretto "github.com/dgraph-io/ristretto"
   518  	freelru "github.com/elastic/go-freelru"
   519  	hashicorp "github.com/hashicorp/golang-lru/v2/expirable"
   520  	ccache "github.com/karlseguin/ccache/v3"
   521  	lxzan "github.com/lxzan/memorycache"
   522  	otter "github.com/maypok86/otter"
   523  	ecache "github.com/orca-zhang/ecache"
   524  	phuslu "github.com/phuslu/lru"
   525  )
   526  
   527  const keysize = 16
   528  var repeat, _ = strconv.Atoi(os.Getenv("repeat"))
   529  
   530  var keys []string
   531  
   532  func main() {
   533  	name := os.Args[1]
   534  	cachesize, _ := strconv.Atoi(os.Args[2])
   535  
   536  	keys = make([]string, cachesize)
   537  	for i := range cachesize {
   538  		keys[i] = fmt.Sprintf(fmt.Sprintf("%%0%dd", keysize), i)
   539  	}
   540  
   541  	map[string]func(int){
   542  		"nottl":      SetupNottl,
   543  		"phuslu":     SetupPhuslu,
   544  		"freelru":    SetupFreelru,
   545  		"ristretto":  SetupRistretto,
   546  		"otter":      SetupOtter,
   547  		"lxzan":      SetupLxzan,
   548  		"ecache":     SetupEcache,
   549  		"cloudflare": SetupCloudflare,
   550  		"ccache":     SetupCcache,
   551  		"hashicorp":  SetupHashicorp,
   552  		"theine":     SetupTheine,
   553  	}[name](cachesize)
   554  }
   555  
   556  func SetupNottl(cachesize int) {
   557  	defer debug.SetGCPercent(debug.SetGCPercent(-1))
   558  	cache := phuslu.NewLRUCache[string, int](cachesize)
   559  	runtime.GC()
   560  	for range repeat {
   561  		for i := range cachesize {
   562  			cache.Set(keys[i], i)
   563  		}
   564  		runtime.GC()
   565  	}
   566  }
   567  
   568  func SetupPhuslu(cachesize int) {
   569  	defer debug.SetGCPercent(debug.SetGCPercent(-1))
   570  	cache := phuslu.NewTTLCache[string, int](cachesize)
   571  	runtime.GC()
   572  	for range repeat {
   573  		for i := range cachesize {
   574  			cache.Set(keys[i], i, time.Hour)
   575  		}
   576  		runtime.GC()
   577  	}
   578  }
   579  
   580  func SetupFreelru(cachesize int) {
   581  	defer debug.SetGCPercent(debug.SetGCPercent(-1))
   582  	cache, _ := freelru.NewSharded[string, int](uint32(cachesize), func(s string) uint32 { return uint32(xxhash.Sum64String(s)) })
   583  	runtime.GC()
   584  	for range repeat {
   585  		for i := range cachesize {
   586  			cache.AddWithLifetime(keys[i], i, time.Hour)
   587  		}
   588  		runtime.GC()
   589  	}
   590  }
   591  
   592  func SetupOtter(cachesize int) {
   593  	defer debug.SetGCPercent(debug.SetGCPercent(-1))
   594  	cache, _ := otter.MustBuilder[string, int](cachesize).WithVariableTTL().Build()
   595  	runtime.GC()
   596  	for range repeat {
   597  		for i := range cachesize {
   598  			cache.Set(keys[i], i, time.Hour)
   599  		}
   600  		runtime.GC()
   601  	}
   602  }
   603  
   604  func SetupEcache(cachesize int) {
   605  	defer debug.SetGCPercent(debug.SetGCPercent(-1))
   606  	cache := ecache.NewLRUCache(1024, uint16(cachesize/1024), time.Hour)
   607  	runtime.GC()
   608  	for range repeat {
   609  		for i := range cachesize {
   610  			cache.Put(keys[i], i)
   611  		}
   612  		runtime.GC()
   613  	}
   614  }
   615  
   616  func SetupRistretto(cachesize int) {
   617  	defer debug.SetGCPercent(debug.SetGCPercent(-1))
   618  	cache, _ := ristretto.NewCache(&ristretto.Config{
   619  		NumCounters: int64(10 * cachesize), // number of keys to track frequency of (10M).
   620  		MaxCost:     int64(cachesize),      // maximum cost of cache (1M).
   621  		BufferItems: 64,                    // number of keys per Get buffer.
   622  	})
   623  	runtime.GC()
   624  	for range repeat {
   625  		for i := range cachesize {
   626  			cache.SetWithTTL(keys[i], i, 1, time.Hour)
   627  		}
   628  		runtime.GC()
   629  	}
   630  }
   631  
   632  func SetupLxzan(cachesize int) {
   633  	defer debug.SetGCPercent(debug.SetGCPercent(-1))
   634  	cache := lxzan.New[string, int](
   635  		lxzan.WithBucketNum(128),
   636  		lxzan.WithBucketSize(cachesize/128, cachesize/128),
   637  		lxzan.WithInterval(time.Hour, time.Hour),
   638  	)
   639  	runtime.GC()
   640  	for range repeat {
   641  		for i := range cachesize {
   642  			cache.Set(keys[i], i, time.Hour)
   643  		}
   644  		runtime.GC()
   645  	}
   646  }
   647  
   648  func SetupTheine(cachesize int) {
   649  	defer debug.SetGCPercent(debug.SetGCPercent(-1))
   650  	cache, _ := theine.NewBuilder[string, int](int64(cachesize)).Build()
   651  	runtime.GC()
   652  	for range repeat {
   653  		for i := range cachesize {
   654  			cache.SetWithTTL(keys[i], i, 1, time.Hour)
   655  		}
   656  		runtime.GC()
   657  	}
   658  }
   659  
   660  func SetupCloudflare(cachesize int) {
   661  	defer debug.SetGCPercent(debug.SetGCPercent(-1))
   662  	cache := cloudflare.NewMultiLRUCache(1024, uint(cachesize/1024))
   663  	runtime.GC()
   664  	for range repeat {
   665  		for i := range cachesize {
   666  			cache.Set(keys[i], i, time.Now().Add(time.Hour))
   667  		}
   668  		runtime.GC()
   669  	}
   670  }
   671  
   672  func SetupCcache(cachesize int) {
   673  	defer debug.SetGCPercent(debug.SetGCPercent(-1))
   674  	cache := ccache.New(ccache.Configure[int]().MaxSize(int64(cachesize)).ItemsToPrune(100))
   675  	runtime.GC()
   676  	for range repeat {
   677  		for i := range cachesize {
   678  			cache.Set(keys[i], i, time.Hour)
   679  		}
   680  		runtime.GC()
   681  	}
   682  }
   683  
   684  func SetupHashicorp(cachesize int) {
   685  	defer debug.SetGCPercent(debug.SetGCPercent(-1))
   686  	cache := hashicorp.NewLRU[string, int](cachesize, nil, time.Hour)
   687  	runtime.GC()
   688  	for range repeat {
   689  		for i := range cachesize {
   690  			cache.Add(keys[i], i)
   691  		}
   692  		runtime.GC()
   693  	}
   694  }
   695  ```
   696  </details>
   697  
   698  | GCScan     | 100000 | 200000 | 400000 | 1000000 |
   699  | ---------- | ------ | ------ | ------ | ------- |
   700  | phuslu     | 1 ms   | 3 ms   | 6 ms   | 15 ms   |
   701  | freelru    | 2 ms   | 3 ms   | 6 ms   | 16 ms   |
   702  | ristretto  | 4 ms   | 6 ms   | 9 ms   | 17 ms   |
   703  | lxzan      | 2 ms   | 4 ms   | 7 ms   | 18 ms   |
   704  | cloudflare | 5 ms   | 10 ms  | 21 ms  | 56 ms   |
   705  | ecache     | 5 ms   | 10 ms  | 21 ms  | 57 ms   |
   706  | ccache     | 5 ms   | 10 ms  | 22 ms  | 58 ms   |
   707  | otter      | 6 ms   | 12 ms  | 28 ms  | 64 ms   |
   708  | hashicorp  | 7 ms   | 16 ms  | 30 ms  | 79 ms   |
   709  | theine     | 6 ms   | 13 ms  | 34 ms  | 83 ms   |
   710  
   711  ### Memory usage
   712  
   713  The Memory usage result as below. Check github [memory][memory] action for more results and details.
   714  <details>
   715    <summary>memory usage on keysize=16(string), valuesize=8(int), cachesize in (100000,200000,400000,1000000,2000000,4000000)</summary>
   716  
   717  ```go
   718  // memusage.go
   719  package main
   720  
   721  import (
   722  	"fmt"
   723  	"os"
   724  	"runtime"
   725  	"time"
   726  	"strconv"
   727  
   728  	theine "github.com/Yiling-J/theine-go"
   729  	"github.com/cespare/xxhash/v2"
   730  	cloudflare "github.com/cloudflare/golibs/lrucache"
   731  	ristretto "github.com/dgraph-io/ristretto"
   732  	freelru "github.com/elastic/go-freelru"
   733  	hashicorp "github.com/hashicorp/golang-lru/v2/expirable"
   734  	ccache "github.com/karlseguin/ccache/v3"
   735  	lxzan "github.com/lxzan/memorycache"
   736  	otter "github.com/maypok86/otter"
   737  	ecache "github.com/orca-zhang/ecache"
   738  	phuslu "github.com/phuslu/lru"
   739  )
   740  
   741  const keysize = 16
   742  
   743  var keys []string
   744  
   745  func main() {
   746  	name := os.Args[1]
   747  	cachesize, _ := strconv.Atoi(os.Args[2])
   748  
   749  	keys = make([]string, cachesize)
   750  	for i := range cachesize {
   751  		keys[i] = fmt.Sprintf(fmt.Sprintf("%%0%dd", keysize), i)
   752  	}
   753  
   754  	var o runtime.MemStats
   755  	runtime.ReadMemStats(&o)
   756  
   757  	map[string]func(int){
   758  		"nottl":      SetupNottl,
   759  		"phuslu":     SetupPhuslu,
   760  		"freelru":    SetupFreelru,
   761  		"ristretto":  SetupRistretto,
   762  		"otter":      SetupOtter,
   763  		"lxzan":      SetupLxzan,
   764  		"ecache":     SetupEcache,
   765  		"cloudflare": SetupCloudflare,
   766  		"ccache":     SetupCcache,
   767  		"hashicorp":  SetupHashicorp,
   768  		"theine":     SetupTheine,
   769  	}[name](cachesize)
   770  
   771  	var m runtime.MemStats
   772  	runtime.ReadMemStats(&m)
   773  
   774  	fmt.Printf("%s\t%d\t%v MB\t%v MB\t%v MB\n",
   775  		name,
   776  		cachesize,
   777  		(m.Alloc-o.Alloc)/1048576,
   778  		(m.TotalAlloc-o.TotalAlloc)/1048576,
   779  		(m.Sys-o.Sys)/1048576,
   780  	)
   781  }
   782  
   783  func SetupNottl(cachesize int) {
   784  	cache := phuslu.NewLRUCache[string, int](cachesize)
   785  	for i := range cachesize {
   786  		cache.Set(keys[i], i)
   787  	}
   788  }
   789  
   790  func SetupPhuslu(cachesize int) {
   791  	cache := phuslu.NewTTLCache[string, int](cachesize)
   792  	for i := range cachesize {
   793  		cache.Set(keys[i], i, time.Hour)
   794  	}
   795  }
   796  
   797  func SetupFreelru(cachesize int) {
   798  	cache, _ := freelru.NewSharded[string, int](uint32(cachesize), func(s string) uint32 { return uint32(xxhash.Sum64String(s)) })
   799  	for i := range cachesize {
   800  		cache.AddWithLifetime(keys[i], i, time.Hour)
   801  	}
   802  }
   803  
   804  func SetupOtter(cachesize int) {
   805  	cache, _ := otter.MustBuilder[string, int](cachesize).WithVariableTTL().Build()
   806  	for i := range cachesize {
   807  		cache.Set(keys[i], i, time.Hour)
   808  	}
   809  }
   810  
   811  func SetupEcache(cachesize int) {
   812  	cache := ecache.NewLRUCache(1024, uint16(cachesize/1024), time.Hour)
   813  	for i := range cachesize {
   814  		cache.Put(keys[i], i)
   815  	}
   816  }
   817  
   818  func SetupRistretto(cachesize int) {
   819  	cache, _ := ristretto.NewCache(&ristretto.Config{
   820  		NumCounters: int64(10 * cachesize), // number of keys to track frequency of (10M).
   821  		MaxCost:     int64(cachesize),      // maximum cost of cache (1M).
   822  		BufferItems: 64,             // number of keys per Get buffer.
   823  	})
   824  	for i := range cachesize {
   825  		cache.SetWithTTL(keys[i], i, 1, time.Hour)
   826  	}
   827  }
   828  
   829  func SetupLxzan(cachesize int) {
   830  	cache := lxzan.New[string, int](
   831  		lxzan.WithBucketNum(128),
   832  		lxzan.WithBucketSize(cachesize/128, cachesize/128),
   833  		lxzan.WithInterval(time.Hour, time.Hour),
   834  	)
   835  	for i := range cachesize {
   836  		cache.Set(keys[i], i, time.Hour)
   837  	}
   838  }
   839  
   840  func SetupTheine(cachesize int) {
   841  	cache, _ := theine.NewBuilder[string, int](int64(cachesize)).Build()
   842  	for i := range cachesize {
   843  		cache.SetWithTTL(keys[i], i, 1, time.Hour)
   844  	}
   845  }
   846  
   847  func SetupCloudflare(cachesize int) {
   848  	cache := cloudflare.NewMultiLRUCache(1024, uint(cachesize/1024))
   849  	for i := range cachesize {
   850  		cache.Set(keys[i], i, time.Now().Add(time.Hour))
   851  	}
   852  }
   853  
   854  func SetupCcache(cachesize int) {
   855  	cache := ccache.New(ccache.Configure[int]().MaxSize(int64(cachesize)).ItemsToPrune(100))
   856  	for i := range cachesize {
   857  		cache.Set(keys[i], i, time.Hour)
   858  	}
   859  }
   860  
   861  func SetupHashicorp(cachesize int) {
   862  	cache := hashicorp.NewLRU[string, int](cachesize, nil, time.Hour)
   863  	for i := range cachesize {
   864  		cache.Add(keys[i], i)
   865  	}
   866  }
   867  ```
   868  </details>
   869  
   870  |            | 100000 | 200000 | 400000 | 1000000 | 2000000 | 4000000 |
   871  | ---------- | ------ | ------ | ------ | ------- | ------- | ------- |
   872  | nottl*     | 3 MB   | 7 MB   | 13 MB  | 39 MB   | 78 MB   | 155 MB  |
   873  | phuslu     | 4 MB   | 8 MB   | 16 MB  | 46 MB   | 93 MB   | 185 MB  |
   874  | lxzan      | 8 MB   | 17 MB  | 35 MB  | 95 MB   | 190 MB  | 379 MB  |
   875  | ristretto* | 14 MB  | 15 MB  | 34 MB  | 89 MB   | 213 MB  | 412 MB  |
   876  | otter      | 13 MB  | 22 MB  | 54 MB  | 104 MB  | 209 MB  | 419 MB  |
   877  | freelru*   | 6 MB   | 14 MB  | 27 MB  | 112 MB  | 224 MB  | 448 MB  |
   878  | ecache     | 11 MB  | 22 MB  | 44 MB  | 123 MB  | 238 MB  | 468 MB  |
   879  | theine     | 15 MB  | 31 MB  | 62 MB  | 178 MB  | 357 MB  | 714 MB  |
   880  | cloudflare | 15 MB  | 33 MB  | 64 MB  | 183 MB  | 358 MB  | 717 MB  |
   881  | ccache     | 16 MB  | 33 MB  | 65 MB  | 183 MB  | 365 MB  | 730 MB  |
   882  | hashicorp  | 18 MB  | 37 MB  | 57 MB  | 242 MB  | 484 MB  | 968 MB  |
   883  - nottl saves 20% memory usage compared to phuslu by removing its ttl functionality, resulting in a slight increase in throughput.
   884  - ristretto employs a questionable usage pattern due to its rejection of items via a bloom filter, resulting in a lower hit ratio.
   885  - freelru overcommits the cache size to the next power of 2, leading to higher memory usage particularly at larger cache sizes.
   886  
   887  ### Hit ratio
   888  It is a classic sharded LRU implementation, so the hit ratio is comparable to or slightly lower than a regular LRU.
   889  
   890  ### License
   891  LRU is licensed under the MIT License. See the LICENSE file for details.
   892  
   893  ### Contact
   894  For inquiries or support, contact phus.lu@gmail.com or raise github issues.
   895  
   896  [godoc-img]: http://img.shields.io/badge/godoc-reference-blue.svg
   897  [godoc]: https://pkg.go.dev/github.com/phuslu/lru
   898  [release-img]: https://img.shields.io/github/v/tag/phuslu/lru?label=release
   899  [release]: https://github.com/phuslu/lru/tags
   900  [goreport-img]: https://goreportcard.com/badge/github.com/phuslu/lru
   901  [goreport]: https://goreportcard.com/report/github.com/phuslu/lru
   902  [benchmark]: https://github.com/phuslu/lru/actions/workflows/benchmark.yml
   903  [memory]: https://github.com/phuslu/lru/actions/workflows/memory.yml
   904  [gcscan]: https://github.com/phuslu/lru/actions/workflows/gcscan.yml
   905  [codecov-img]: https://codecov.io/gh/phuslu/lru/graph/badge.svg?token=Q21AMQNM1K
   906  [codecov]: https://codecov.io/gh/phuslu/lru