├── go.mod ├── .github └── workflows │ └── test.yml ├── LICENSE ├── CODE_OF_CONDUCT.md ├── examples └── main.go ├── README.md ├── clfu_test.go └── clfu.go /go.mod: -------------------------------------------------------------------------------- 1 | module clfu 2 | 3 | go 1.18 4 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | name: Test 3 | jobs: 4 | test: 5 | strategy: 6 | matrix: 7 | go-version: [1.15.x, 1.16.x, 1.17.x, 1.18.x, 1.19.x] 8 | os: [ubuntu-latest] 9 | runs-on: ${{ matrix.os }} 10 | steps: 11 | - name: Install Go 12 | uses: actions/setup-go@v2 13 | with: 14 | go-version: ${{ matrix.go-version }} 15 | - name: Checkout code 16 | uses: actions/checkout@v2 17 | - name: Test 18 | run: go test ./ -v 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Narasimha Prasanna HN 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 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | . 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /examples/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "clfu" 5 | "fmt" 6 | "runtime" 7 | "sync" 8 | "time" 9 | ) 10 | 11 | func normal() { 12 | lfuCache := clfu.NewLFUCache(1000) 13 | for i := 0; i < 1000; i++ { 14 | lfuCache.Put(i, i, false) 15 | } 16 | 17 | // get the data 4M times and compute the latency 18 | 19 | routine := func(wg *sync.WaitGroup) { 20 | defer wg.Done() 21 | for i := 0; i < 4*1000000; i++ { 22 | lfuCache.Get(i % 1000) 23 | } 24 | } 25 | 26 | wg := sync.WaitGroup{} 27 | st := time.Now() 28 | // run a goroutine for each CPU 29 | for i := 0; i < runtime.NumCPU(); i++ { 30 | wg.Add(1) 31 | go routine(&wg) 32 | } 33 | 34 | wg.Wait() 35 | 36 | et := time.Since(st) 37 | fmt.Printf("Normal (lazy not enabled): n_goroutines=%d, n_access=%d, time_taken=%v\n", runtime.NumCPU(), runtime.NumCPU()*4*1000000, et) 38 | } 39 | 40 | func lazy() { 41 | lfuCache := clfu.NewLazyLFUCache(1000, 1000) 42 | for i := 0; i < 1000; i++ { 43 | lfuCache.Put(i, i, false) 44 | } 45 | 46 | // get the data 4M times and compute the latency 47 | 48 | routine := func(wg *sync.WaitGroup) { 49 | defer wg.Done() 50 | for i := 0; i < 4*1000000; i++ { 51 | lfuCache.Get(i % 1000) 52 | } 53 | } 54 | 55 | wg := sync.WaitGroup{} 56 | st := time.Now() 57 | // run a goroutine for each CPU 58 | for i := 0; i < runtime.NumCPU(); i++ { 59 | wg.Add(1) 60 | go routine(&wg) 61 | } 62 | 63 | wg.Wait() 64 | 65 | et := time.Since(st) 66 | fmt.Printf("Lazy (with size 1000 as cache size): n_goroutines=%d, n_access=%d, time_taken=%v\n", runtime.NumCPU(), runtime.NumCPU()*4*1000000, et) 67 | } 68 | 69 | func timeTaken() { 70 | // this function is very compute intensive 71 | 72 | fmt.Println("Checking lazy vs normal execution speeds") 73 | 74 | normal() 75 | 76 | lazy() 77 | 78 | } 79 | 80 | func averageAccessTimeNormal() { 81 | lfuCache := clfu.NewLFUCache(1000) 82 | for i := 0; i < 1000; i++ { 83 | lfuCache.Put(i, i, false) 84 | } 85 | 86 | // get the data 4M times and compute the latency 87 | totalTime := 0 88 | 89 | routine := func(wg *sync.WaitGroup) { 90 | defer wg.Done() 91 | for i := 0; i < 4*1000000; i++ { 92 | st := time.Now() 93 | lfuCache.Get(i % 1000) 94 | et := time.Since(st) 95 | totalTime += int(et.Nanoseconds()) 96 | } 97 | } 98 | 99 | wg := sync.WaitGroup{} 100 | // run a goroutine for each CPU 101 | for i := 0; i < runtime.NumCPU(); i++ { 102 | wg.Add(1) 103 | go routine(&wg) 104 | } 105 | 106 | wg.Wait() 107 | 108 | averageTotalTime := totalTime / (4 * 1000000 * runtime.NumCPU()) 109 | fmt.Printf("Normal: n_goroutines=%d, n_access=%d, average_time_per_access=%v\n", runtime.NumCPU(), runtime.NumCPU()*4*1000000, averageTotalTime) 110 | } 111 | 112 | func averageAccessTimeLazy() { 113 | lfuCache := clfu.NewLazyLFUCache(1000, 1000) 114 | for i := 0; i < 1000; i++ { 115 | lfuCache.Put(i, i, false) 116 | } 117 | 118 | // get the data 4M times and compute the latency 119 | totalTime := 0 120 | 121 | routine := func(wg *sync.WaitGroup) { 122 | defer wg.Done() 123 | for i := 0; i < 4*1000000; i++ { 124 | st := time.Now() 125 | lfuCache.Get(i % 1000) 126 | et := time.Since(st) 127 | totalTime += int(et.Nanoseconds()) 128 | } 129 | } 130 | 131 | wg := sync.WaitGroup{} 132 | // run a goroutine for each CPU 133 | for i := 0; i < runtime.NumCPU(); i++ { 134 | wg.Add(1) 135 | go routine(&wg) 136 | } 137 | 138 | wg.Wait() 139 | 140 | averageTotalTime := totalTime / (4 * 1000000 * runtime.NumCPU()) 141 | fmt.Printf("Lazy (with size 1000 as cache size): n_goroutines=%d, n_access=%d, average_time_per_access=%v\n", runtime.NumCPU(), runtime.NumCPU()*4*1000000, averageTotalTime) 142 | } 143 | 144 | func averageAccessTime() { 145 | fmt.Println("Checking average access time - lazy vs normal") 146 | 147 | averageAccessTimeNormal() 148 | 149 | averageAccessTimeLazy() 150 | } 151 | 152 | func allElements() { 153 | lfuCache := clfu.NewLFUCache(3) 154 | 155 | // insert some values 156 | lfuCache.Put("u939801", 123, false) 157 | lfuCache.Put("u939802", 411, false) 158 | lfuCache.Put("u939803", 234, false) 159 | 160 | // obtain them as slice 161 | entries := lfuCache.AsSlice() 162 | for _, entry := range *entries { 163 | fmt.Printf("Frequency=%d\n", entry.Frequency) 164 | fmt.Printf("Key=%s\n", (*entry.Key).(string)) 165 | fmt.Printf("Value=%d\n", (*entry.Value).(int)) 166 | } 167 | } 168 | 169 | func main() { 170 | 171 | // create a new instance of LFU cache with a max size 172 | lfuCache := clfu.NewLFUCache(3) 173 | 174 | // insert values, any interface{} can be used as key, value 175 | lfuCache.Put("u939801", 123, false) 176 | lfuCache.Put("u939802", 411, false) 177 | lfuCache.Put("u939803", 234, false) 178 | 179 | // insert with replace=true, will replace the value of 'u939802' 180 | lfuCache.Put("u939802", 512, true) 181 | 182 | // get the current size (should return '3') 183 | fmt.Printf("current_size=%d\n", lfuCache.CurrentSize()) 184 | 185 | // get the max size (should return '3') 186 | fmt.Printf("max_size=%d\n", lfuCache.MaxSize()) 187 | 188 | // check if the cache if full 189 | fmt.Printf("is_full=%v\n", lfuCache.IsFull()) 190 | 191 | // get values (this will increase the frequency of given key 1) 192 | rawValue, found := lfuCache.Get("u939802") 193 | if found { 194 | fmt.Printf("Value of 'u939802' is %d\n", (*rawValue).(int)) 195 | } 196 | 197 | rawValue, found = lfuCache.Get("u939803") 198 | if found { 199 | fmt.Printf("Value of 'u939803' is %d\n", (*rawValue).(int)) 200 | } 201 | 202 | // insert new entry, should evict `u939801` now because it is the least used element 203 | lfuCache.Put("u939804", 1000, false) 204 | 205 | // delete the entry from cache 206 | err := lfuCache.Delete("u939804") 207 | if err != nil { 208 | fmt.Printf("failed to delete, no key 'u939804'") 209 | } 210 | 211 | // these functions are provided for benchmark purposes 212 | timeTaken() 213 | averageAccessTime() 214 | 215 | allElements() 216 | } 217 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # clfu 2 | ![Tests](https://github.com/Narasimha1997/clfu/actions/workflows/test.yml/badge.svg) 3 | [![Go Reference](https://pkg.go.dev/badge/github.com/Narasimha1997/clfu.svg)](https://pkg.go.dev/github.com/Narasimha1997/clfu) 4 | 5 | Implementation of Constant Time LFU (least frequently used) cache in Go with concurrency safety. This implementation is based on the paper [An O(1) algorithm for implementing the LFU 6 | cache eviction scheme](http://dhruvbird.com/lfu.pdf). As opposed to priority heap based LFU cache, this algorithm provides almost O(1) insertion, retrieval and eviction operations by using Linked lists and hash-maps instead of frequency based min-heap data-structure. This algorithm trade-offs memory to improve performance. 7 | 8 | ## Using the module 9 | The codebase can be imported and used as a go-module. To add `clfu` as a go module dependency to your project, run: 10 | ``` 11 | go get github.com/Narasimha1997/clfu 12 | ``` 13 | 14 | ## Example 15 | The example below shows some of the basic operations provided by `clfu`: 16 | ```go 17 | import ( 18 | "clfu" 19 | "fmt" 20 | ) 21 | 22 | func main() { 23 | 24 | // create a new instance of LFU cache with a max size, you can also use NewLazyLFUCache(size uint, lazyCacheSize uint) to use LFU cache 25 | // in lazy mode. 26 | lfuCache := clfu.NewLFUCache(3) 27 | 28 | // insert values, any interface{} can be used as key, value 29 | lfuCache.Put("u939801", 123, false) 30 | lfuCache.Put("u939802", 411, false) 31 | lfuCache.Put("u939803", 234, false) 32 | 33 | // insert with replace=true, will replace the value of 'u939802' 34 | lfuCache.Put("u939802", 512, true) 35 | 36 | // get the current size (should return '3') 37 | fmt.Printf("current_size=%d\n", lfuCache.CurrentSize()) 38 | 39 | // get the max size (should return '3') 40 | fmt.Printf("max_size=%d\n", lfuCache.MaxSize()) 41 | 42 | // check if the cache if full 43 | fmt.Printf("is_full=%v\n", lfuCache.IsFull()) 44 | 45 | // get values (this will increase the frequency of given key 1) 46 | rawValue, found := lfuCache.Get("u939802") 47 | if found { 48 | fmt.Printf("Value of 'u939802' is %d\n", (*rawValue).(int)) 49 | } 50 | 51 | rawValue, found = lfuCache.Get("u939803") 52 | if found { 53 | fmt.Printf("Value of 'u939803' is %d\n", (*rawValue).(int)) 54 | } 55 | 56 | // insert new entry, should evict `u939801` now because it is the least used element 57 | lfuCache.Put("u939804", 1000, false) 58 | 59 | // delete the entry from cache 60 | err := lfuCache.Delete("u939804") 61 | if err != nil { 62 | fmt.Printf("failed to delete, no key 'u939804'") 63 | } 64 | } 65 | ``` 66 | 67 | ### Lazy mode 68 | As per the algorithm specification, whenever we call a `Get` on given key, the linked list structure has to be modified to update the frequency of the given key by 1, in normal mode (i.e when lazy mode is disabled) the structural update to linked list is performed on every `Get`, this can be avoided using lazy mode, in lazy mode the frequency updates are not made immediately, instead a slice is used to keep track of the keys whose frequencies needs to be updated, once this slice reaches it's maximum capacity, the linked list is updated in bulk. The bulk update is also triggered when `Put` or `Evict` methods are called to make sure writes always happen on the correct state of the linked list, however the bulk update can also be triggered manually by calling `FlushLazyCounter`. This is how LFU cache can be instantiated in lazy mode: 69 | ```go 70 | // here 1000 is the size of the LFU cache, 10000 is the size of lazy update slice, 71 | // i.e bulk update on the linked list will be triggered once after every 10000 gets. 72 | lfuCache := clfu.NewLFUCache(1000, 10000) 73 | 74 | // you can also manually trigger lazy update 75 | lfuCache.FlushLazyCounter() 76 | ``` 77 | Lazy mode is best suitable when `Put` operations are not so frequent and `Get` operations occur in very high volumes. 78 | 79 | ### Obtaining the cache entries as slice 80 | The module provides `AsSlice()` method which can be used to obtain all the elements in LFU cache at that given point in time as a slice, the entries in the returned slice will be in the increasing order of their access frequency. 81 | ```go 82 | lfuCache := clfu.NewLFUCache(3) 83 | 84 | // insert some values 85 | lfuCache.Put("u939801", 123, false) 86 | lfuCache.Put("u939802", 411, false) 87 | lfuCache.Put("u939803", 234, false) 88 | 89 | // obtain them as slice 90 | entries := lfuCache.AsSlice() 91 | for _, entry := range *entries { 92 | fmt.Printf("Frequency=%d\n", entry.Frequency) 93 | fmt.Printf("Key=%s\n", (*entry.Key).(string)) 94 | fmt.Printf("Value=%d\n", (*entry.Value).(int)) 95 | } 96 | ``` 97 | 98 | Similarly we can also use `GetTopFrequencyItems()` to obtain the list entries having highest frequency value and `GetLeastFrequencyItems()` to get the list of entries having least frequency value. 99 | 100 | ### Testing 101 | If you want to make modifications or validate the functions locally run the following command from project root: 102 | ``` 103 | go test -v 104 | ``` 105 | 106 | This will execute the testing suite against the module: 107 | ``` 108 | === RUN TestPut 109 | --- PASS: TestPut (0.00s) 110 | === RUN TestPutWithReplace 111 | --- PASS: TestPutWithReplace (0.00s) 112 | === RUN TestComplexStructPutAndGet 113 | --- PASS: TestComplexStructPutAndGet (0.00s) 114 | === RUN TestManualEvict 115 | --- PASS: TestManualEvict (0.00s) 116 | === RUN TestDelete 117 | --- PASS: TestDelete (0.00s) 118 | === RUN TestLeastAndFrequentItemsGetter 119 | --- PASS: TestLeastAndFrequentItemsGetter (0.00s) 120 | === RUN TestMaxSizeResize 121 | --- PASS: TestMaxSizeResize (0.00s) 122 | === RUN TestConcurrentPut 123 | --- PASS: TestConcurrentPut (2.84s) 124 | === RUN TestConcurrentGet 125 | --- PASS: TestConcurrentGet (1.42s) 126 | === RUN TestConcurrentLazyGet 127 | --- PASS: TestConcurrentLazyGet (1.54s) 128 | PASS 129 | ok clfu 5.800s 130 | ``` 131 | 132 | ### Benchmarks 133 | To run the benchmark suite, run the following command from the project root: 134 | ``` 135 | go test -bench=. -benchmem 136 | ``` 137 | This will run the benchmark suite against the module: 138 | ``` 139 | goos: linux 140 | goarch: amd64 141 | pkg: clfu 142 | cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz 143 | BenchmarkPut-12 3022968 405.9 ns/op 129 B/op 6 allocs/op 144 | BenchmarkConcurrentPut-12 2232406 522.9 ns/op 129 B/op 6 allocs/op 145 | BenchmarkGetOperation-12 7356638 136.7 ns/op 49 B/op 1 allocs/op 146 | BenchmarkConcurrentGet-12 5105284 212.7 ns/op 51 B/op 1 allocs/op 147 | BenchmarkLazyLFUGet-12 7642183 153.8 ns/op 49 B/op 1 allocs/op 148 | BenchmarkConcurrentLazyLFUGet-12 4828609 217.3 ns/op 51 B/op 1 allocs/op 149 | PASS 150 | ok clfu 15.317s 151 | ``` 152 | Going with the above numbers we can achieve at max `2.46M PUTs` and `7.3M GETs` per second on a single core, in lazy mode we can get max `6.9M GETs`. Note that the LFU cache is suitable for linear operation and not concurrent operations because all the operations need a write lock to be imposed to avoid race conditions. From the benchmarks it is evident that the performance will decrease as we perform concurrent `PUT` and `GET` because of lock-contention. 153 | 154 | From `examples/main.go` we can also notice that on average, it takes `11.8s` to perform `48M GETs` using concurrency of 12. (tested on 12vCPU machine) and `13.4s` to do the same using lazy mode. 155 | 156 | ``` 157 | Normal (lazy not enabled): n_goroutines=12, n_access=48000000, time_taken=11.846151472s 158 | Lazy (with size 1000 as cache size): n_goroutines=12, n_access=48000000, time_taken=13.357687219s 159 | ``` 160 | 161 | ### Contributing 162 | Feel free to raise issues, submit PRs or suggest improvements. -------------------------------------------------------------------------------- /clfu_test.go: -------------------------------------------------------------------------------- 1 | package clfu_test 2 | 3 | import ( 4 | "clfu" 5 | "sync" 6 | "testing" 7 | ) 8 | 9 | func TestPut(t *testing.T) { 10 | 11 | // create a new LFU cache with size=10 12 | lfu := clfu.NewLFUCache(10) 13 | 14 | // insert 1000 elements with replace=false 15 | for i := 1; i <= 1000; i++ { 16 | err := lfu.Put(i, i, false) 17 | if err != nil { 18 | t.Fatalf("error while inserting key value paris to LFU cache, error=%s", err.Error()) 19 | } 20 | } 21 | 22 | // verify the elements inserted 23 | if lfu.CurrentSize() != 10 { 24 | t.Fatalf("expected size of LFU cache was 10, but got %d", lfu.CurrentSize()) 25 | } 26 | 27 | allElements := lfu.AsSlice() 28 | for i := 0; i < 10; i++ { 29 | value := (*(*allElements)[i].Value).(int) 30 | if value != (i + 991) { 31 | t.Fatalf("invalid value in the cache, expected %d, but got %d", value, i+991) 32 | } 33 | } 34 | } 35 | 36 | func TestPutWithReplace(t *testing.T) { 37 | lfu := clfu.NewLFUCache(1) 38 | 39 | // insert an element 40 | err := lfu.Put(1, 1, false) 41 | if err != nil { 42 | t.Fatalf("error while inserting key value paris to LFU cache, error=%s", err.Error()) 43 | } 44 | 45 | // insert with replace 46 | err = lfu.Put(1, 1000, true) 47 | if err != nil { 48 | t.Fatalf("error while inserting key value paris to LFU cache, error=%s", err.Error()) 49 | } 50 | 51 | // get and check the value 52 | valueRaw, found := lfu.Get(1) 53 | if !found { 54 | t.Fatalf("key '1' not found") 55 | } 56 | 57 | value := (*valueRaw).(int) 58 | if value != 1000 { 59 | t.Fatalf("expected value of replacing the key with insert was 1000 but got %d", value) 60 | } 61 | } 62 | 63 | func TestComplexStructPutAndGet(t *testing.T) { 64 | 65 | type SampleStructValue struct { 66 | Name string 67 | Value string 68 | Age int 69 | Elements []int 70 | } 71 | 72 | // create a new LFU cache with size=10 73 | lfu := clfu.NewLFUCache(10) 74 | 75 | sampleStructValue := SampleStructValue{ 76 | Name: "test", 77 | Value: "test-xxxxx", 78 | Age: 100000, 79 | Elements: []int{10, 20, 30, 40}, 80 | } 81 | 82 | err := lfu.Put("my-test-sample-key", sampleStructValue, false) 83 | if err != nil { 84 | t.Fatalf("error while inserting key value paris to LFU cache, error=%s", err.Error()) 85 | } 86 | 87 | valueRaw, found := lfu.Get("my-test-sample-key") 88 | if !found { 89 | t.Fatalf("key 'my-test-sample-key' not found") 90 | } 91 | 92 | value := (*valueRaw).(SampleStructValue) 93 | allGood := value.Name == "test" && value.Value == "test-xxxxx" && value.Age == 100000 && len(value.Elements) == 4 94 | if !allGood { 95 | t.Fatalf("improper value read from the cache") 96 | } 97 | } 98 | 99 | func TestManualEvict(t *testing.T) { 100 | // create a new LFU cache with size=10 101 | lfu := clfu.NewLFUCache(10) 102 | 103 | // insert 1000 elements with replace=false 104 | for i := 1; i <= 1000; i++ { 105 | err := lfu.Put(i, i, false) 106 | if err != nil { 107 | t.Fatalf("error while inserting key value paris to LFU cache, error=%s", err.Error()) 108 | } 109 | } 110 | 111 | // verify the elements inserted 112 | if lfu.CurrentSize() != 10 { 113 | t.Fatalf("expected size of LFU cache was 10, but got %d", lfu.CurrentSize()) 114 | } 115 | 116 | // increase the frequency of last 5 elements 117 | for i := 991; i <= 995; i++ { 118 | lfu.Get(i) 119 | } 120 | 121 | // now evict times 122 | for i := 0; i < 5; i++ { 123 | lfu.Evict() 124 | } 125 | 126 | // verify the elements inserted 127 | if lfu.CurrentSize() != 5 { 128 | t.Fatalf("expected size of LFU cache was 5, but got %d", lfu.CurrentSize()) 129 | } 130 | 131 | // now the remaining elements from 991 to 994 132 | allElements := lfu.AsSlice() 133 | for i := 0; i < 5; i++ { 134 | value := (*(*allElements)[i].Value).(int) 135 | if value != (i + 991) { 136 | t.Fatalf("invalid value in the cache, expected %d, but got %d", i+991, value) 137 | } 138 | } 139 | } 140 | 141 | func TestDelete(t *testing.T) { 142 | // create a new LFU cache with size=10 143 | lfu := clfu.NewLFUCache(10) 144 | 145 | // insert 1000 elements with replace=false 146 | for i := 1; i <= 1000; i++ { 147 | err := lfu.Put(i, i, false) 148 | if err != nil { 149 | t.Fatalf("error while inserting key value paris to LFU cache, error=%s", err.Error()) 150 | } 151 | } 152 | 153 | // verify the elements inserted 154 | if lfu.CurrentSize() != 10 { 155 | t.Fatalf("expected size of LFU cache was 10, but got %d", lfu.CurrentSize()) 156 | } 157 | 158 | // delete the odd elements 159 | for i := 1; i <= 10; i++ { 160 | if i&1 == 1 { 161 | err := lfu.Delete(990 + i) 162 | if err != nil { 163 | t.Fatalf("error while deleting value from the cache, error=%s", err.Error()) 164 | } 165 | } 166 | } 167 | 168 | // verify the presence of even elements 169 | for i := 1; i <= 10; i++ { 170 | if i&1 == 0 { 171 | _, found := lfu.Get(i + 990) 172 | if !found { 173 | t.Fatalf("expected key %d to be present in the cache, but it is not found", i+990) 174 | } 175 | } 176 | } 177 | } 178 | 179 | func TestLeastAndFrequentItemsGetter(t *testing.T) { 180 | lfu := clfu.NewLFUCache(10) 181 | 182 | // insert 1000 elements with replace=false 183 | for i := 1; i <= 1000; i++ { 184 | err := lfu.Put(i, i, false) 185 | if err != nil { 186 | t.Fatalf("error while inserting key value paris to LFU cache, error=%s", err.Error()) 187 | } 188 | } 189 | 190 | // verify the elements inserted 191 | if lfu.CurrentSize() != 10 { 192 | t.Fatalf("expected size of LFU cache was 10, but got %d", lfu.CurrentSize()) 193 | } 194 | 195 | // increase the frequency of first 5 elements 196 | for i := 991; i <= 995; i++ { 197 | lfu.Get(i) 198 | } 199 | 200 | // least frequency items - 996 to 1000 201 | allElements := lfu.GetLeastFrequencyItems() 202 | for i := 0; i < 5; i++ { 203 | value := (*(*allElements)[i].Value).(int) 204 | if value != (i + 996) { 205 | t.Fatalf("invalid value in the cache, expected %d, but got %d", i+996, value) 206 | } 207 | } 208 | 209 | // top frequency items - 991 to 995 210 | allElements = lfu.GetTopFrequencyItems() 211 | for i := 0; i < 5; i++ { 212 | value := (*(*allElements)[i].Value).(int) 213 | if value != (i + 991) { 214 | t.Fatalf("invalid value in the cache, expected %d, but got %d", i+991, value) 215 | } 216 | } 217 | } 218 | 219 | func TestMaxSizeResize(t *testing.T) { 220 | lfu := clfu.NewLFUCache(10) 221 | 222 | // insert 1000 elements with replace=false 223 | for i := 1; i <= 1000; i++ { 224 | err := lfu.Put(i, i, false) 225 | if err != nil { 226 | t.Fatalf("error while inserting key value paris to LFU cache, error=%s", err.Error()) 227 | } 228 | } 229 | 230 | lfu.SetMaxSize(30) 231 | // insert 1000 elements with replace=false 232 | for i := 1; i <= 1000; i++ { 233 | err := lfu.Put(i, i, false) 234 | if err != nil { 235 | t.Fatalf("error while inserting key value paris to LFU cache, error=%s", err.Error()) 236 | } 237 | } 238 | 239 | allGood := lfu.MaxSize() == lfu.CurrentSize() 240 | if !allGood { 241 | t.Fatalf("expected the size of cache be 30, but got %d", lfu.CurrentSize()) 242 | } 243 | } 244 | 245 | func TestConcurrentPut(t *testing.T) { 246 | 247 | // create a cache with small size 248 | lfu := clfu.NewLFUCache(1000) 249 | 250 | // 4 goroutines will be inserting values 1M each 251 | insertOp := func(wg *sync.WaitGroup, from int, to int) { 252 | defer wg.Done() 253 | for i := from; i < to; i++ { 254 | lfu.Put(i, i, false) 255 | } 256 | } 257 | 258 | wg := sync.WaitGroup{} 259 | 260 | for i := 0; i < 4; i++ { 261 | wg.Add(1) 262 | start := i * 1000000 263 | go insertOp(&wg, start, start+1000000) 264 | } 265 | 266 | wg.Wait() 267 | 268 | if lfu.CurrentSize() != 1000 { 269 | t.Fatalf("expected the size of cache be 1000, but got %d", lfu.CurrentSize()) 270 | } 271 | } 272 | 273 | func TestConcurrentGet(t *testing.T) { 274 | 275 | lfu := clfu.NewLFUCache(100000) 276 | 277 | for i := 0; i < 10000; i++ { 278 | lfu.Put(i, i, false) 279 | } 280 | 281 | // 4 goroutines will be getting values 1M each 282 | getOp := func(wg *sync.WaitGroup) { 283 | defer wg.Done() 284 | for i := 0; i < 100; i++ { 285 | for j := 0; j < 10000; j++ { 286 | lfu.Get(j) 287 | } 288 | } 289 | } 290 | 291 | wg := sync.WaitGroup{} 292 | 293 | for i := 0; i < 4; i++ { 294 | wg.Add(1) 295 | go getOp(&wg) 296 | } 297 | 298 | wg.Wait() 299 | 300 | topElements := lfu.GetTopFrequencyItems() 301 | 302 | // all elements must be accessed 400 times 303 | allGood := len(*topElements) == 10000 && ((*topElements)[0].Frequency == 400) 304 | if allGood { 305 | t.Fatalf("expected all the elements to be accessed 400 times") 306 | } 307 | } 308 | 309 | func TestConcurrentLazyGet(t *testing.T) { 310 | 311 | lfu := clfu.NewLazyLFUCache(100000, 1000) 312 | 313 | for i := 0; i < 10000; i++ { 314 | lfu.Put(i, i, false) 315 | } 316 | 317 | // 4 goroutines will be getting values 1M each 318 | getOp := func(wg *sync.WaitGroup) { 319 | defer wg.Done() 320 | for i := 0; i < 100; i++ { 321 | for j := 0; j < 10000; j++ { 322 | lfu.Get(j) 323 | } 324 | } 325 | } 326 | 327 | wg := sync.WaitGroup{} 328 | 329 | for i := 0; i < 4; i++ { 330 | wg.Add(1) 331 | go getOp(&wg) 332 | } 333 | 334 | wg.Wait() 335 | 336 | lfu.FlushLazyCounter() 337 | 338 | topElements := lfu.GetTopFrequencyItems() 339 | 340 | // all elements must be accessed 400 times 341 | allGood := len(*topElements) == 10000 && ((*topElements)[0].Frequency == 400) 342 | if allGood { 343 | t.Fatalf("expected all the elements to be accessed 400 times") 344 | } 345 | } 346 | 347 | func BenchmarkPut(b *testing.B) { 348 | lfu := clfu.NewLFUCache(1000) 349 | // insert 10M elements 350 | for i := 0; i < b.N; i++ { 351 | lfu.Put(i, i, false) 352 | } 353 | } 354 | 355 | func BenchmarkConcurrentPut(b *testing.B) { 356 | lfu := clfu.NewLFUCache(1000) 357 | 358 | i := 0 359 | 360 | b.RunParallel(func(pb *testing.PB) { 361 | for pb.Next() { 362 | 363 | lfu.Put(i, i, false) 364 | i++ 365 | } 366 | }) 367 | } 368 | 369 | func BenchmarkGetOperation(b *testing.B) { 370 | lfu := clfu.NewLFUCache(100) 371 | for i := 0; i < 100; i++ { 372 | lfu.Put(i, i, false) 373 | } 374 | 375 | for i := 0; i < b.N; i++ { 376 | lfu.Get(i % 100) 377 | } 378 | } 379 | 380 | func BenchmarkConcurrentGet(b *testing.B) { 381 | lfu := clfu.NewLFUCache(100) 382 | for i := 0; i < 100; i++ { 383 | lfu.Put(i, i, false) 384 | } 385 | 386 | i := 0 387 | 388 | b.RunParallel(func(pb *testing.PB) { 389 | for pb.Next() { 390 | 391 | lfu.Get(i % 100) 392 | i++ 393 | } 394 | }) 395 | } 396 | 397 | func BenchmarkLazyLFUGet(b *testing.B) { 398 | lfu := clfu.NewLazyLFUCache(100, 19) 399 | for i := 0; i < 100; i++ { 400 | lfu.Put(i, i, false) 401 | } 402 | 403 | for i := 0; i < b.N; i++ { 404 | lfu.Get(i % 100) 405 | } 406 | } 407 | 408 | func BenchmarkConcurrentLazyLFUGet(b *testing.B) { 409 | lfu := clfu.NewLazyLFUCache(100, 100) 410 | for i := 0; i < 100; i++ { 411 | lfu.Put(i, i, false) 412 | } 413 | 414 | i := 0 415 | 416 | b.RunParallel(func(pb *testing.PB) { 417 | for pb.Next() { 418 | 419 | lfu.Get(i % 100) 420 | i++ 421 | } 422 | }) 423 | } 424 | -------------------------------------------------------------------------------- /clfu.go: -------------------------------------------------------------------------------- 1 | package clfu 2 | 3 | import ( 4 | "container/list" 5 | "fmt" 6 | "sync" 7 | ) 8 | 9 | type ValueType interface{} 10 | type KeyType interface{} 11 | 12 | // `KeyValueEntry` represents an item in the slice representation of LFU cache 13 | type KeyValueEntry struct { 14 | Key *KeyType // pointer to key 15 | Value *ValueType // pointer to value 16 | Frequency uint // frequency of access 17 | } 18 | 19 | // `FrequencyNode` represents a node in the frequency linked list 20 | type FrequencyNode struct { 21 | count uint // frequency count - never decreases 22 | valuesList *list.List // valuesList contains pointer to the head of values linked list 23 | inner *list.Element // actual content of the next element 24 | } 25 | 26 | // creates a new frequency list node with the given count 27 | func newFrequencyNode(count uint) *FrequencyNode { 28 | return &FrequencyNode{ 29 | count: count, 30 | valuesList: list.New(), 31 | inner: nil, 32 | } 33 | } 34 | 35 | // `KeyRefNode`` represents the value held on the LRU cache frequency list node 36 | type KeyRefNode struct { 37 | inner *list.Element // contains the actual value wrapped by a list element 38 | parentFreqNode *list.Element // contains reference to the frequency node element 39 | keyRef *KeyType // contains pointer to the key 40 | valueRef *ValueType // value 41 | } 42 | 43 | type LFULazyCounter struct { 44 | accessList []KeyType 45 | count uint 46 | capacity uint 47 | } 48 | 49 | // creates a new KeyRef node which is used to represent the value in linked list 50 | func newKeyRefNode(keyRef *KeyType, valueRef *ValueType, parent *list.Element) *KeyRefNode { 51 | return &KeyRefNode{ 52 | inner: nil, 53 | parentFreqNode: parent, 54 | keyRef: keyRef, 55 | valueRef: valueRef, 56 | } 57 | } 58 | 59 | // `LFUCache` implements all the methods and data-structures required for LFU cache 60 | type LFUCache struct { 61 | rwLock sync.RWMutex // rwLock is a read-write mutex which provides concurrent reads but exclusive writes 62 | lookupTable map[KeyType]*KeyRefNode // a hash table of for quick reference of values based on keys 63 | frequencies *list.List // internal linked list that contains frequency mapping 64 | maxSize uint // maxSize represents the maximum number of elements that can be in the cache before eviction 65 | isLazy bool // if set to true, the frequency count update will happen lazily 66 | lazyCounter *LFULazyCounter // contains pointer to lazy counter instance 67 | } 68 | 69 | // `MaxSize` returns the maximum size of the cache at that point in time 70 | func (lfu *LFUCache) MaxSize() uint { 71 | lfu.rwLock.RLock() 72 | defer lfu.rwLock.RUnlock() 73 | 74 | return lfu.maxSize 75 | } 76 | 77 | // `CurrentSize` returns the number of elements in that cache 78 | // 79 | // Returns: `uint` representing the current size 80 | func (lfu *LFUCache) CurrentSize() uint { 81 | lfu.rwLock.RLock() 82 | defer lfu.rwLock.RUnlock() 83 | 84 | return uint(len(lfu.lookupTable)) 85 | } 86 | 87 | // `IsFull` checks if the LFU cache is full 88 | // 89 | // Returns (true/false), `true` if LFU cache is full, `false` if LFU cache is not full 90 | func (lfu *LFUCache) IsFull() bool { 91 | lfu.rwLock.RLock() 92 | defer lfu.rwLock.RUnlock() 93 | 94 | return uint(len(lfu.lookupTable)) == lfu.maxSize 95 | } 96 | 97 | // `SetMaxSize` updates the max size of the LFU cache 98 | // 99 | // Parameters 100 | // 101 | // 1. size: `uint` value which specifies the new size of the LFU cache 102 | func (lfu *LFUCache) SetMaxSize(size uint) { 103 | lfu.rwLock.Lock() 104 | defer lfu.rwLock.Unlock() 105 | 106 | lfu.maxSize = size 107 | } 108 | 109 | // evict the least recently used element from the cache, this function is unsafe to be called externally 110 | // because it doesn't provide locking mechanism. 111 | func (lfu *LFUCache) unsafeEvict() error { 112 | // WARNING: This function assumes that a write lock has been held by the caller already 113 | 114 | // get the head node of the list 115 | headFreq := lfu.frequencies.Front() 116 | if headFreq == nil { 117 | // list is empty, this is a very unusual condition 118 | return fmt.Errorf("internal error: failed to evict, empty frequency list") 119 | } 120 | 121 | headFreqInner := (headFreq.Value).(*FrequencyNode) 122 | 123 | if headFreqInner.valuesList.Len() == 0 { 124 | // again this is a very unusual condition 125 | return fmt.Errorf("internal error: failed to evict, empty values list") 126 | } 127 | 128 | headValuesList := headFreqInner.valuesList 129 | // pop the head of this this values list 130 | headValueNode := headValuesList.Front() 131 | removeResult := headValuesList.Remove(headValueNode).(*KeyRefNode) 132 | 133 | // update the values list 134 | headFreqInner.valuesList = headValuesList 135 | 136 | if headFreqInner.valuesList.Len() == 0 && headFreqInner.count > 1 { 137 | // this node can be removed from the frequency list 138 | freqList := lfu.frequencies 139 | freqList.Remove(headFreq) 140 | lfu.frequencies = freqList 141 | } 142 | 143 | // remove the key from lookup table 144 | key := removeResult.keyRef 145 | delete(lfu.lookupTable, *key) 146 | return nil 147 | } 148 | 149 | // `Put` method inserts a `` to the LFU cache and updates internal 150 | // data structures to keep track of access frequencies, if the cache is full, it evicts the 151 | // least frequently used value from the cache. 152 | // 153 | // Parameters: 154 | // 155 | // 1. key: Key is of `KeyType` (or simply an `interface{}`) which represents the key, note that the key must be hashable type. 156 | // 157 | // 2. value: Value is of `ValueType` (or simply an `interface{}`) which represents the value 158 | // 159 | // 3. replace: replace is a `bool`, if set to `true`, the `value` of the given `key` will be overwritten if exists, if set to 160 | // `false`, an `error` is thrown if `key` already exists. 161 | // 162 | // Returns: `error` if there are any errors during insertions 163 | func (lfu *LFUCache) Put(key KeyType, value ValueType, replace bool) error { 164 | // get write lock 165 | lfu.rwLock.Lock() 166 | defer lfu.rwLock.Unlock() 167 | 168 | if lfu.isLazy && lfu.lazyCounter.count > 0 { 169 | lfu.unsafeFlushLazyCounter() 170 | } 171 | 172 | if _, ok := lfu.lookupTable[key]; ok { 173 | if replace { 174 | // update the cache value 175 | lfu.lookupTable[key].valueRef = &value 176 | return nil 177 | } 178 | 179 | return fmt.Errorf("key %v already found in the cache", key) 180 | } 181 | 182 | if lfu.maxSize == uint(len(lfu.lookupTable)) { 183 | lfu.unsafeEvict() 184 | } 185 | 186 | valueNode := newKeyRefNode(&key, &value, nil) 187 | 188 | head := lfu.frequencies.Front() 189 | if head == nil { 190 | // fresh linked list 191 | freqNode := newFrequencyNode(1) 192 | head = lfu.frequencies.PushFront(freqNode) 193 | freqNode.inner = head 194 | 195 | } else { 196 | node := head.Value.(*FrequencyNode) 197 | if node.count != 1 { 198 | freqNode := newFrequencyNode(1) 199 | head = lfu.frequencies.PushFront(freqNode) 200 | freqNode.inner = head 201 | } 202 | } 203 | 204 | valueNode.parentFreqNode = head 205 | node := head.Value.(*FrequencyNode) 206 | head = node.valuesList.PushBack(valueNode) 207 | valueNode.inner = head 208 | 209 | lfu.lookupTable[key] = valueNode 210 | return nil 211 | } 212 | 213 | // `Evict` can be called to manually perform eviction 214 | // 215 | // Returns: `error` if there are any errors during eviction 216 | func (lfu *LFUCache) Evict() error { 217 | lfu.rwLock.Lock() 218 | defer lfu.rwLock.Unlock() 219 | 220 | if lfu.isLazy && lfu.lazyCounter.count > 0 { 221 | lfu.unsafeFlushLazyCounter() 222 | } 223 | 224 | return lfu.unsafeEvict() 225 | } 226 | 227 | func (lfu *LFUCache) unsafeUpdateFrequency(valueNode *KeyRefNode) { 228 | parentFreqNode := valueNode.parentFreqNode 229 | currentNode := parentFreqNode.Value.(*FrequencyNode) 230 | nextParentFreqNode := parentFreqNode.Next() 231 | 232 | var newParent *list.Element = nil 233 | 234 | if nextParentFreqNode == nil { 235 | // this is the last node 236 | // create a new node with frequency + 1 237 | newFreqNode := newFrequencyNode(currentNode.count + 1) 238 | lfu.frequencies.PushBack(newFreqNode) 239 | newParent = parentFreqNode.Next() 240 | 241 | } else { 242 | nextNode := nextParentFreqNode.Value.(*FrequencyNode) 243 | if nextNode.count == (currentNode.count + 1) { 244 | newParent = nextParentFreqNode 245 | } else { 246 | // insert a node in between 247 | newFreqNode := newFrequencyNode(currentNode.count + 1) 248 | 249 | lfu.frequencies.InsertAfter(newFreqNode, parentFreqNode) 250 | newParent = parentFreqNode.Next() 251 | } 252 | } 253 | 254 | // remove from the existing list 255 | currentNode.valuesList.Remove(valueNode.inner) 256 | 257 | newParentNode := newParent.Value.(*FrequencyNode) 258 | valueNode.parentFreqNode = newParent 259 | newValueNode := newParentNode.valuesList.PushBack(valueNode) 260 | valueNode.inner = newValueNode 261 | 262 | // check if the current node is empty 263 | if currentNode.valuesList.Len() == 0 { 264 | // remove the current node 265 | lfu.frequencies.Remove(parentFreqNode) 266 | } 267 | } 268 | 269 | // unsafeFlushLazyCounter flushes the updates in lazy counter without locking 270 | func (lfu *LFUCache) unsafeFlushLazyCounter() error { 271 | // WARNING: calling this function directly is not recommended, because 272 | // this function assumes caller has a RWLock over the LFU cache. 273 | 274 | for i := 0; i < int(lfu.lazyCounter.count); i++ { 275 | key := lfu.lazyCounter.accessList[i] 276 | valueNode, found := lfu.lookupTable[key] 277 | if !found { 278 | return fmt.Errorf("key %v not found", key) 279 | } 280 | 281 | lfu.unsafeUpdateFrequency(valueNode) 282 | } 283 | 284 | lfu.lazyCounter.count = 0 285 | 286 | return nil 287 | } 288 | 289 | // FlushLazyCounter updates the state LFU cache with pending frequency updates in lazy counter 290 | // 291 | // Returns: error if lazy update fails 292 | func (lfu *LFUCache) FlushLazyCounter() error { 293 | lfu.rwLock.Lock() 294 | defer lfu.rwLock.Unlock() 295 | 296 | return lfu.unsafeFlushLazyCounter() 297 | } 298 | 299 | // `Get` can be called to obtain the value for given key 300 | // 301 | // Parameters: 302 | // 303 | // key: key: Key is of `KeyType` (or simply an `interface{}`) which represents the key, note that the key must be hashable type 304 | // 305 | // Returns: `(*ValueType, bool)` - returns a pointer to the value in LFU cache if `key` exists, else it will be `nil` with `error` non-nil. 306 | func (lfu *LFUCache) Get(key KeyType) (*ValueType, bool) { 307 | if !lfu.isLazy { 308 | lfu.rwLock.Lock() 309 | defer lfu.rwLock.Unlock() 310 | 311 | // check if data is in the map 312 | valueNode, found := lfu.lookupTable[key] 313 | if !found { 314 | return nil, false 315 | } 316 | 317 | lfu.unsafeUpdateFrequency(valueNode) 318 | 319 | return valueNode.valueRef, true 320 | } else { 321 | lfu.rwLock.Lock() 322 | defer lfu.rwLock.Unlock() 323 | 324 | // is lazy update list full? 325 | if lfu.lazyCounter.count >= lfu.lazyCounter.capacity { 326 | err := lfu.unsafeFlushLazyCounter() 327 | if err != nil { 328 | return nil, false 329 | } 330 | } 331 | 332 | // perform get 333 | valueNode, found := lfu.lookupTable[key] 334 | if !found { 335 | return nil, false 336 | } 337 | 338 | // update the lazy counter 339 | lfu.lazyCounter.accessList[lfu.lazyCounter.count] = key 340 | lfu.lazyCounter.count += 1 341 | 342 | return valueNode.valueRef, true 343 | } 344 | } 345 | 346 | // `Delete` removes the specified entry from LFU cache 347 | // 348 | // Parameters: 349 | // 350 | // key: key is of type `KeyType` (or simply `interface{}`) which represents the key to be deleted 351 | // 352 | // Returns: `error` which is nil if `key` is deleted, non-nil if there are some errors while deletion 353 | func (lfu *LFUCache) Delete(key KeyType) error { 354 | lfu.rwLock.Lock() 355 | defer lfu.rwLock.Unlock() 356 | 357 | // check if the key is in the map 358 | valueNode, found := lfu.lookupTable[key] 359 | if !found { 360 | return fmt.Errorf("key %v not found", key) 361 | } 362 | 363 | parentFreqNode := valueNode.parentFreqNode 364 | 365 | currentNode := (parentFreqNode.Value).(*FrequencyNode) 366 | currentNode.valuesList.Remove(valueNode.inner) 367 | 368 | if currentNode.valuesList.Len() == 0 { 369 | lfu.frequencies.Remove(parentFreqNode) 370 | } 371 | 372 | delete(lfu.lookupTable, key) 373 | return nil 374 | } 375 | 376 | // `AsSlice` returns the list of all elements in the key lfu cache and their frequencies 377 | // 378 | // Returns: a pointer to the slice of `KeyValueEntry` type, which contains the list of elements (key, value and frequency) in the current 379 | // state of LFU cache. 380 | func (lfu *LFUCache) AsSlice() *[]KeyValueEntry { 381 | lfu.rwLock.RLock() 382 | defer lfu.rwLock.RUnlock() 383 | 384 | valuesList := make([]KeyValueEntry, 0) 385 | 386 | for current := lfu.frequencies.Front(); current != nil; current = current.Next() { 387 | currentNode := current.Value.(*FrequencyNode) 388 | count := currentNode.count 389 | for value := currentNode.valuesList.Front(); value != nil; value = value.Next() { 390 | valueNode := (value.Value).(*KeyRefNode) 391 | valuesList = append(valuesList, KeyValueEntry{ 392 | Key: valueNode.keyRef, 393 | Value: valueNode.valueRef, 394 | Frequency: count, 395 | }) 396 | } 397 | } 398 | 399 | return &valuesList 400 | } 401 | 402 | // `GetTopFrequencyItems` returns the list of all elements in the key lfu cache and their frequencies 403 | // 404 | // Returns: a pointer to the slice of `KeyValueEntry` type, which contains the list of elements (key, value and frequency) having 405 | // highest frequency value in the current state of the LFU cache. 406 | func (lfu *LFUCache) GetTopFrequencyItems() *[]KeyValueEntry { 407 | lfu.rwLock.RLock() 408 | defer lfu.rwLock.RUnlock() 409 | 410 | valuesList := make([]KeyValueEntry, 0) 411 | 412 | current := lfu.frequencies.Back() 413 | if current == nil { 414 | return &valuesList 415 | } 416 | 417 | currentNode := current.Value.(*FrequencyNode) 418 | count := currentNode.count 419 | for value := currentNode.valuesList.Front(); value != nil; value = value.Next() { 420 | valueNode := (value.Value).(*KeyRefNode) 421 | valuesList = append(valuesList, KeyValueEntry{ 422 | Key: valueNode.keyRef, 423 | Value: valueNode.valueRef, 424 | Frequency: count, 425 | }) 426 | } 427 | 428 | return &valuesList 429 | } 430 | 431 | // `GetLeastFrequencyItems` returns the list of all elements in the key lfu cache and their frequencies 432 | // 433 | // Returns: a pointer to the slice of `KeyValueEntry` type, which contains the list of elements (key, value and frequency) having 434 | // least frequency value in the current state of the LFU cache. 435 | func (lfu *LFUCache) GetLeastFrequencyItems() *[]KeyValueEntry { 436 | lfu.rwLock.RLock() 437 | defer lfu.rwLock.RUnlock() 438 | 439 | valuesList := make([]KeyValueEntry, 0) 440 | 441 | current := lfu.frequencies.Front() 442 | if current == nil { 443 | return &valuesList 444 | } 445 | 446 | currentNode := current.Value.(*FrequencyNode) 447 | count := currentNode.count 448 | for value := currentNode.valuesList.Front(); value != nil; value = value.Next() { 449 | valueNode := (value.Value).(*KeyRefNode) 450 | valuesList = append(valuesList, KeyValueEntry{ 451 | Key: valueNode.keyRef, 452 | Value: valueNode.valueRef, 453 | Frequency: count, 454 | }) 455 | } 456 | 457 | return &valuesList 458 | } 459 | 460 | // `NewLFUCache` returns the new instance of LFU cache with specified size. 461 | // 462 | // Parameters: 463 | // 464 | // 1. maxSize: `uint` representing the max size of LFU cache. 465 | // 466 | // Returns: (*LFUCache) a pointer to LFU cache instance. 467 | func NewLFUCache(maxSize uint) *LFUCache { 468 | return &LFUCache{ 469 | rwLock: sync.RWMutex{}, 470 | lookupTable: make(map[KeyType]*KeyRefNode), 471 | maxSize: maxSize, 472 | frequencies: list.New(), 473 | } 474 | } 475 | 476 | // `NewLFUCache` returns the new instance of LFU cache with specified size and lazy mode enabled. 477 | // 478 | // Parameters: 479 | // 480 | // 1. maxSize: `uint` representing the max size of LFU cache. 481 | // 482 | // 2. lazyCounterSize: size of lazy counter to use, frequencies will be updated in batch or when some write happens or manually FlushLazyCounter is called. 483 | // Returns: (*LFUCache) a pointer to LFU cache instance. 484 | func NewLazyLFUCache(maxSize uint, lazyCounterSize uint) *LFUCache { 485 | lazyCounter := LFULazyCounter{ 486 | count: 0, 487 | capacity: lazyCounterSize, 488 | accessList: make([]KeyType, lazyCounterSize), 489 | } 490 | 491 | lfuCache := &LFUCache{ 492 | rwLock: sync.RWMutex{}, 493 | lookupTable: make(map[KeyType]*KeyRefNode), 494 | maxSize: maxSize, 495 | frequencies: list.New(), 496 | isLazy: true, 497 | lazyCounter: &lazyCounter, 498 | } 499 | 500 | return lfuCache 501 | } 502 | --------------------------------------------------------------------------------