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