├── .github ├── CODE_OF_CONDUCT.md └── workflows │ └── ci.yml ├── .gitignore ├── LICENSE ├── README.md ├── example_test.go ├── go.mod ├── go.sum ├── lru.go └── lru_test.go /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | Please read the [Go Community Code of Conduct](https://golang.org/conduct). -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - main 7 | push: 8 | tags: 9 | - v* 10 | branches: 11 | - main 12 | 13 | jobs: 14 | build: 15 | runs-on: ${{ matrix.os }} 16 | strategy: 17 | matrix: 18 | os: [ubuntu-latest, windows-latest] 19 | go: [1.18] 20 | steps: 21 | - uses: actions/checkout@v2 22 | 23 | - name: Set up Go 24 | uses: actions/setup-go@v2 25 | with: 26 | go-version: ${{ matrix.go }} 27 | 28 | - name: Build 29 | run: go install 30 | 31 | - name: golangci-lint 32 | uses: golangci/golangci-lint-action@v3 33 | with: 34 | version: latest 35 | 36 | - name: Test 37 | run: | 38 | go test -v -cover ./... -coverprofile coverage.out -coverpkg ./... 39 | go tool cover -func coverage.out -o coverage.out # Replaces coverage.out with the analysis of coverage.out 40 | 41 | - name: Go Coverage Badge 42 | uses: tj-actions/coverage-badge-go@v1 43 | if: ${{ runner.os == 'Linux' && matrix.go == '1.18' }} # Runs this on only one of the ci builds. 44 | with: 45 | green: 80 46 | filename: coverage.out 47 | 48 | - uses: stefanzweifel/git-auto-commit-action@v4 49 | id: auto-commit-action 50 | with: 51 | commit_message: Apply Code Coverage Badge 52 | skip_fetch: true 53 | skip_checkout: true 54 | file_pattern: ./README.md 55 | 56 | - name: Push Changes 57 | if: steps.auto-commit-action.outputs.changes_detected == 'true' 58 | uses: ad-m/github-push-action@master 59 | with: 60 | github_token: ${{ github.token }} 61 | branch: ${{ github.ref }} 62 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Vsevolod Strukchinsky 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # lru 2 | [![Go Reference](https://pkg.go.dev/badge/github.com/floatdrop/lru.svg)](https://pkg.go.dev/github.com/floatdrop/lru) 3 | [![CI](https://github.com/floatdrop/lru/actions/workflows/ci.yml/badge.svg)](https://github.com/floatdrop/lru/actions/workflows/ci.yml) 4 | ![Coverage](https://img.shields.io/badge/Coverage-100.0%25-brightgreen) 5 | [![Go Report Card](https://goreportcard.com/badge/github.com/floatdrop/lru)](https://goreportcard.com/report/github.com/floatdrop/lru) 6 | 7 | Thread safe GoLang LRU cache. 8 | 9 | ## Example 10 | 11 | ```go 12 | import ( 13 | "fmt" 14 | 15 | "github.com/floatdrop/lru" 16 | ) 17 | 18 | func main() { 19 | cache := lru.New[string, int](256) 20 | 21 | cache.Set("Hello", 5) 22 | 23 | if e := cache.Get("Hello"); e != nil { 24 | fmt.Println(*e) 25 | // Output: 5 26 | } 27 | } 28 | ``` 29 | 30 | ## TTL 31 | 32 | You can wrap values into `Expiring[T any]` struct to release memory on timer (or manually in `Valid` method). 33 | 34 |
35 | Example implementation 36 | 37 | ```go 38 | import ( 39 | "fmt" 40 | "time" 41 | 42 | "github.com/floatdrop/lru" 43 | ) 44 | 45 | type Expiring[T any] struct { 46 | value *T 47 | } 48 | 49 | func (E *Expiring[T]) Valid() *T { 50 | if E == nil { 51 | return nil 52 | } 53 | 54 | return E.value 55 | } 56 | 57 | func WithTTL[T any](value T, ttl time.Duration) Expiring[T] { 58 | e := Expiring[T]{ 59 | value: &value, 60 | } 61 | 62 | time.AfterFunc(ttl, func() { 63 | e.value = nil // Release memory 64 | }) 65 | 66 | return e 67 | } 68 | 69 | func main() { 70 | l := lru.New[string, Expiring[string]](256) 71 | 72 | l.Set("Hello", WithTTL("Bye", time.Hour)) 73 | 74 | if e := l.Get("Hello").Valid(); e != nil { 75 | fmt.Println(*e) 76 | } 77 | } 78 | ``` 79 | 80 | **Note:** Althou this short implementation frees memory after ttl duration, it will not erase entry for key in cache. It can be a problem, if you do not check nillnes after getting element from cache and call `Set` afterwards. 81 |
82 | 83 | ## Benchmarks 84 | 85 | ``` 86 | floatdrop/lru: 87 | BenchmarkLRU_Rand-8 8802915 131.7 ns/op 24 B/op 1 allocs/op 88 | BenchmarkLRU_Freq-8 9392769 127.8 ns/op 24 B/op 1 allocs/op 89 | 90 | hashicorp/golang-lru: 91 | BenchmarkLRU_Rand-8 5992782 195.8 ns/op 76 B/op 3 allocs/op 92 | BenchmarkLRU_Freq-8 6355358 186.1 ns/op 71 B/op 3 allocs/op 93 | 94 | jellydator/ttlcache: 95 | BenchmarkLRU_Rand-8 4447654 253.5 ns/op 144 B/op 2 allocs/op 96 | BenchmarkLRU_Freq-8 4837938 240.9 ns/op 137 B/op 2 allocs/op 97 | ``` 98 | -------------------------------------------------------------------------------- /example_test.go: -------------------------------------------------------------------------------- 1 | package lru_test 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/floatdrop/lru" 7 | ) 8 | 9 | func ExampleLRU() { 10 | cache := lru.New[string, int](256) 11 | 12 | cache.Set("Hello", 5) 13 | 14 | if e := cache.Get("Hello"); e != nil { 15 | fmt.Println(*cache.Get("Hello")) 16 | // Output: 5 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/floatdrop/lru 2 | 3 | go 1.18 4 | 5 | require github.com/bahlo/generic-list-go v0.2.0 6 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= 2 | github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= 3 | -------------------------------------------------------------------------------- /lru.go: -------------------------------------------------------------------------------- 1 | // Package lru implements cache with least recent used eviction policy. 2 | package lru 3 | 4 | import ( 5 | "sync" 6 | 7 | list "github.com/bahlo/generic-list-go" 8 | ) 9 | 10 | // LRU implements Cache interface with least recent used eviction policy. 11 | type LRU[K comparable, V any] struct { 12 | m sync.Mutex 13 | ll *list.List[*entry[K, V]] 14 | cache map[K]*list.Element[*entry[K, V]] 15 | size int 16 | } 17 | 18 | type entry[K comparable, V any] struct { 19 | key K 20 | value *V 21 | } 22 | 23 | // Evicted holds key/value pair that was evicted from cache. 24 | type Evicted[K comparable, V any] struct { 25 | Key K 26 | Value V 27 | } 28 | 29 | // Get returns pointer to value for key, if value was in cache (nil returned otherwise). 30 | func (L *LRU[K, V]) Get(key K) *V { 31 | L.m.Lock() 32 | defer L.m.Unlock() 33 | 34 | if e, ok := L.cache[key]; ok { 35 | L.ll.MoveToFront(e) 36 | return e.Value.value 37 | } 38 | 39 | return nil 40 | } 41 | 42 | // Set inserts key value pair and returns evicted value, if cache was full. 43 | // If cache size is less than 1 – method will always return reference to value (as if it was immediately evicted). 44 | func (L *LRU[K, V]) Set(key K, value V) *Evicted[K, V] { 45 | if L.size < 1 { 46 | return &Evicted[K, V]{key, value} 47 | } 48 | 49 | L.m.Lock() 50 | defer L.m.Unlock() 51 | 52 | if e, ok := L.cache[key]; ok { 53 | previousValue := e.Value.value 54 | L.ll.MoveToFront(e) 55 | e.Value.value = &value 56 | return &Evicted[K, V]{key, *previousValue} 57 | } 58 | 59 | e := L.ll.Back() 60 | i := e.Value 61 | evictedKey := i.key 62 | evictedValue := i.value 63 | delete(L.cache, i.key) 64 | 65 | i.key = key 66 | i.value = &value 67 | L.cache[key] = e 68 | L.ll.MoveToFront(e) 69 | if evictedValue != nil { 70 | return &Evicted[K, V]{evictedKey, *evictedValue} 71 | } 72 | return nil 73 | } 74 | 75 | // Len returns number of cached items. 76 | func (L *LRU[K, V]) Len() int { 77 | L.m.Lock() 78 | defer L.m.Unlock() 79 | 80 | return len(L.cache) 81 | } 82 | 83 | // Remove method removes entry associated with key and returns pointer to removed value (or nil if entry was not in cache). 84 | func (L *LRU[K, V]) Remove(key K) *V { 85 | L.m.Lock() 86 | defer L.m.Unlock() 87 | 88 | if e, ok := L.cache[key]; ok { 89 | value := e.Value.value 90 | L.ll.MoveToBack(e) 91 | e.Value.value = nil 92 | delete(L.cache, key) 93 | return value 94 | } 95 | 96 | return nil 97 | } 98 | 99 | // Peek returns value for key (if key was in cache), but does not modify its recency. 100 | func (L *LRU[K, V]) Peek(key K) *V { 101 | L.m.Lock() 102 | defer L.m.Unlock() 103 | 104 | if e, ok := L.cache[key]; ok { 105 | return e.Value.value 106 | } 107 | 108 | return nil 109 | } 110 | 111 | // Victim returns pointer to a key, that will be evicted on next Set call (or nil if there is a space for another key). 112 | // If cache size is 0 - nil will be always returned. 113 | func (L *LRU[K, V]) Victim() *K { 114 | if L.size < 1 { 115 | return nil 116 | } 117 | 118 | L.m.Lock() 119 | defer L.m.Unlock() 120 | 121 | e := L.ll.Back() 122 | i := e.Value 123 | evictedKey := i.key 124 | evictedValue := i.value 125 | 126 | if evictedValue != nil { 127 | return &evictedKey 128 | } 129 | 130 | return nil 131 | } 132 | 133 | // New creates LRU cache with size capacity. Cache will preallocate size count of internal structures to avoid allocation in process. 134 | func New[K comparable, V any](size int) *LRU[K, V] { 135 | c := &LRU[K, V]{ 136 | ll: list.New[*entry[K, V]](), 137 | cache: make(map[K]*list.Element[*entry[K, V]], size), 138 | size: size, 139 | } 140 | 141 | for i := 0; i < size; i++ { 142 | c.ll.PushBack(&entry[K, V]{}) 143 | } 144 | 145 | return c 146 | } 147 | -------------------------------------------------------------------------------- /lru_test.go: -------------------------------------------------------------------------------- 1 | package lru 2 | 3 | import ( 4 | "math/rand" 5 | "testing" 6 | ) 7 | 8 | func BenchmarkLRU_Rand(b *testing.B) { 9 | l := New[int64, int64](8192) 10 | 11 | trace := make([]int64, b.N*2) 12 | for i := 0; i < b.N*2; i++ { 13 | trace[i] = rand.Int63() % 32768 14 | } 15 | 16 | b.ResetTimer() 17 | 18 | var hit, miss int 19 | for i := 0; i < 2*b.N; i++ { 20 | if i%2 == 0 { 21 | l.Set(trace[i], trace[i]) 22 | } else { 23 | if l.Get(trace[i]) == nil { 24 | miss++ 25 | } else { 26 | hit++ 27 | } 28 | } 29 | } 30 | b.Logf("hit: %d miss: %d ratio: %f", hit, miss, float64(hit)/float64(miss)) 31 | } 32 | 33 | func BenchmarkLRU_Freq(b *testing.B) { 34 | l := New[int64, int64](8192) 35 | 36 | trace := make([]int64, b.N*2) 37 | for i := 0; i < b.N*2; i++ { 38 | if i%2 == 0 { 39 | trace[i] = rand.Int63() % 16384 40 | } else { 41 | trace[i] = rand.Int63() % 32768 42 | } 43 | } 44 | 45 | b.ResetTimer() 46 | 47 | for i := 0; i < b.N; i++ { 48 | l.Set(trace[i], trace[i]) 49 | } 50 | var hit, miss int 51 | for i := 0; i < b.N; i++ { 52 | if l.Get(trace[i]) == nil { 53 | miss++ 54 | } else { 55 | hit++ 56 | } 57 | } 58 | b.Logf("hit: %d miss: %d ratio: %f", hit, miss, float64(hit)/float64(miss)) 59 | } 60 | 61 | func TestLRU_zero(t *testing.T) { 62 | l := New[int, int](0) 63 | i := 5 64 | 65 | if l.Victim() != nil { 66 | t.Errorf("should have no victims in zero cache") 67 | } 68 | 69 | if e := l.Set(i, i); e == nil || e.Value != i { 70 | t.Fatalf("value should be evicted") 71 | } 72 | 73 | if e := l.Remove(i); e != nil { 74 | t.Fatalf("value should not be removed") 75 | } 76 | } 77 | 78 | func TestLRU_defaultkey(t *testing.T) { 79 | l := New[string, int](1) 80 | var k string 81 | v := 10 82 | 83 | if e := l.Set(k, v); e != nil { 84 | t.Fatalf("value should not be evicted") 85 | } 86 | 87 | if e := l.Get(k); e == nil || *e != v { 88 | t.Fatalf("bad returned value: %v != %v", e, v) 89 | } 90 | } 91 | 92 | func TestLRU_setget(t *testing.T) { 93 | l := New[int, int](128) 94 | 95 | if e := l.Get(5); e != nil { 96 | t.Fatalf("bad returned value: %v != nil", e) 97 | } 98 | 99 | if l.Set(5, 10) != nil { 100 | t.Fatal("should not have evictions") 101 | } 102 | 103 | if e := l.Get(5); *e != 10 { 104 | t.Fatalf("bad returned value: %v != %v", *e, 10) 105 | } 106 | 107 | if e := l.Set(5, 9); e == nil || e.Value != 10 { 108 | t.Fatal("old value should be evicted") 109 | } 110 | } 111 | 112 | func TestLRU_eviction(t *testing.T) { 113 | l := New[int, int](128) 114 | 115 | evictCounter := 0 116 | for i := 0; i < 256; i++ { 117 | if l.Set(i, i) != nil { 118 | evictCounter++ 119 | } 120 | } 121 | 122 | if l.Len() != 128 { 123 | t.Fatalf("bad len: %v", l.Len()) 124 | } 125 | 126 | if evictCounter != 128 { 127 | t.Fatalf("bad evict count: %v", evictCounter) 128 | } 129 | 130 | for i := 0; i < 128; i++ { 131 | if e := l.Get(i); e != nil { 132 | t.Fatalf("should be evicted") 133 | } 134 | } 135 | 136 | for i := 128; i < 256; i++ { 137 | if e := l.Get(i); e == nil { 138 | t.Fatalf("should not be evicted") 139 | } 140 | } 141 | 142 | for i := 128; i < 192; i++ { 143 | l.Remove(i) 144 | if e := l.Get(i); e != nil { 145 | t.Fatalf("should be deleted") 146 | } 147 | } 148 | } 149 | 150 | func TestLRU_peek(t *testing.T) { 151 | l := New[int, int](2) 152 | 153 | l.Set(1, 1) 154 | l.Set(2, 2) 155 | if v := l.Peek(1); v == nil || *v != 1 { 156 | t.Errorf("1 should be set to 1: %v,", v) 157 | } 158 | 159 | l.Set(3, 3) 160 | if l.Peek(1) != nil { 161 | t.Errorf("should not have updated recent-ness of 1") 162 | } 163 | } 164 | 165 | func TestLRU_victim(t *testing.T) { 166 | l := New[int, int](2) 167 | if l.Victim() != nil { 168 | t.Errorf("should have no victims in empty cache") 169 | } 170 | 171 | l.Set(1, 1) 172 | if l.Victim() != nil { 173 | t.Errorf("victim should be nil in not full cache") 174 | } 175 | 176 | l.Set(2, 2) 177 | if v := l.Peek(1); v == nil || *v != 1 { 178 | t.Errorf("1 should be set to 1: %v,", v) 179 | } 180 | 181 | if v := l.Victim(); v == nil || *v != 1 { 182 | t.Errorf("victim should be set to oldest item") 183 | } 184 | } 185 | --------------------------------------------------------------------------------