├── go.mod ├── LICENSE ├── plru.go ├── plru_test.go ├── README.md └── img ├── plru_1.svg ├── plru_2.svg ├── plru_3.svg ├── plru_5.svg ├── plru_4.svg └── plru_6.svg /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/karlmcguire/plru 2 | 3 | go 1.18 4 | 5 | require github.com/pingcap/go-ycsb v0.0.0-20220316035207-968940ea1017 6 | 7 | require ( 8 | github.com/magiconair/properties v1.8.0 // indirect 9 | github.com/pingcap/errors v0.11.5-0.20211224045212-9687c2b0f87c // indirect 10 | go.uber.org/atomic v1.9.0 // indirect 11 | ) 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Karl McGuire 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 | -------------------------------------------------------------------------------- /plru.go: -------------------------------------------------------------------------------- 1 | // A Pseudo-LRU implementation using 1-bit per entry and hit ratio performance 2 | // nearly identical to full LRU. 3 | package plru 4 | 5 | import ( 6 | "math" 7 | "sync/atomic" 8 | ) 9 | 10 | const ( 11 | bSize = 32 12 | bMask = bSize - 1 13 | bFull = math.MaxUint32 14 | ) 15 | 16 | type Policy struct { 17 | blocks []uint32 18 | cursor uint64 19 | size uint64 20 | } 21 | 22 | // NewPolicy returns an empty Policy where size is the number of entries to 23 | // track. The size param is rounded up to the next multiple of 32. 24 | func NewPolicy(size uint64) *Policy { 25 | size = uint64(int64(size+bMask) & int64(-bSize)) 26 | return &Policy{ 27 | blocks: make([]uint32, size/bSize), 28 | size: size, 29 | } 30 | } 31 | 32 | func (p *Policy) Size() uint64 { 33 | return p.size 34 | } 35 | 36 | // Has returns true if the bit is set (1) and false if not (0). 37 | func (p *Policy) Has(bit uint64) bool { 38 | if bit > p.size { 39 | return false 40 | } 41 | return (atomic.LoadUint32(&p.blocks[bit/bSize]) & (1 << (bit & bMask))) > 0 42 | } 43 | 44 | // Hit sets the bit to 1 and clears the other bits in the block if capacity is 45 | // reached. 46 | func (p *Policy) Hit(bit uint64) { 47 | if bit > p.size { 48 | return 49 | } 50 | block := &p.blocks[bit/bSize] 51 | hit: 52 | o := atomic.LoadUint32(block) 53 | n := o | 1<<(bit&bMask) 54 | if n == bFull { 55 | n = 0 | 1<<(bit&bMask) 56 | } 57 | if !atomic.CompareAndSwapUint32(block, o, n) { 58 | goto hit 59 | } 60 | } 61 | 62 | // Del sets the bit to 0. 63 | func (p *Policy) Del(bit uint64) { 64 | if bit > p.size { 65 | return 66 | } 67 | block := &p.blocks[bit/bSize] 68 | del: 69 | o := atomic.LoadUint32(block) 70 | n := o & ^(1 << (bit & bMask)) 71 | if !atomic.CompareAndSwapUint32(block, o, n) { 72 | goto del 73 | } 74 | } 75 | 76 | // Evict returns a LRU bit that you can later pass to Hit. 77 | func (p *Policy) Evict() uint64 { 78 | i := (atomic.AddUint64(&p.cursor, 1) - 1) % uint64(len(p.blocks)) 79 | block := atomic.LoadUint32(&p.blocks[i]) 80 | return (i * bSize) + lookup(^block&(block+1)) 81 | } 82 | 83 | func lookup(b uint32) uint64 { 84 | switch b { 85 | case 1: 86 | return 0 87 | case 2: 88 | return 1 89 | case 4: 90 | return 2 91 | case 8: 92 | return 3 93 | case 16: 94 | return 4 95 | case 32: 96 | return 5 97 | case 64: 98 | return 6 99 | case 128: 100 | return 7 101 | case 256: 102 | return 8 103 | case 512: 104 | return 9 105 | case 1024: 106 | return 10 107 | case 2048: 108 | return 11 109 | case 4096: 110 | return 12 111 | case 8192: 112 | return 13 113 | case 16384: 114 | return 14 115 | case 32768: 116 | return 15 117 | case 65536: 118 | return 16 119 | case 131072: 120 | return 17 121 | case 262144: 122 | return 18 123 | case 524288: 124 | return 19 125 | case 1048576: 126 | return 20 127 | case 2097152: 128 | return 21 129 | case 4194304: 130 | return 22 131 | case 8388608: 132 | return 23 133 | case 16777216: 134 | return 24 135 | case 33554432: 136 | return 25 137 | case 67108864: 138 | return 26 139 | case 134217728: 140 | return 27 141 | case 268435456: 142 | return 28 143 | case 536870912: 144 | return 29 145 | case 1073741824: 146 | return 30 147 | case 2147483648: 148 | return 31 149 | } 150 | panic("invalid bit lookup") 151 | } 152 | -------------------------------------------------------------------------------- /plru_test.go: -------------------------------------------------------------------------------- 1 | package plru 2 | 3 | import ( 4 | "math/rand" 5 | "testing" 6 | "time" 7 | 8 | gen "github.com/pingcap/go-ycsb/pkg/generator" 9 | ) 10 | 11 | func init() { 12 | rand.Seed(time.Now().Unix()) 13 | } 14 | 15 | func TestHas(t *testing.T) { 16 | p := NewPolicy(128) 17 | p.Hit(1) 18 | p.Hit(64) 19 | if p.Has(0) || !p.Has(1) || !p.Has(64) { 20 | t.Fatal("Hit or Has not working") 21 | } 22 | } 23 | 24 | func TestHit(t *testing.T) { 25 | p := NewPolicy(129) 26 | if p.Has(0) { 27 | t.Fatal("Has not working") 28 | } 29 | p.Hit(0) 30 | if !p.Has(0) { 31 | t.Fatal("Hit not working") 32 | } 33 | p.Hit(120) 34 | if !p.Has(120) { 35 | t.Fatal("Hit not working") 36 | } 37 | } 38 | 39 | func TestDel(t *testing.T) { 40 | p := NewPolicy(64) 41 | p.Hit(1) 42 | p.Hit(2) 43 | p.Del(1) 44 | if p.Has(1) { 45 | t.Fatal("Clear not working") 46 | } 47 | if !p.Has(2) { 48 | t.Fatal("Clear other bit") 49 | } 50 | } 51 | 52 | func TestEvict(t *testing.T) { 53 | p := NewPolicy(128) 54 | for i := 0; i < 128; i++ { 55 | victim := p.Evict() 56 | if p.Has(victim) { 57 | t.Fatal("Evict returning used block") 58 | } 59 | p.Hit(p.Evict()) 60 | } 61 | defer func() { 62 | if r := recover(); r == nil { 63 | t.Fatal("lookup should panic") 64 | } 65 | }() 66 | lookup(3) 67 | } 68 | 69 | const ( 70 | benchPolicySize = 1e6 71 | ) 72 | 73 | func benchAccess() (bits [benchPolicySize]uint64) { 74 | z := gen.NewScrambledZipfian(0, benchPolicySize-1, gen.ZipfianConstant) 75 | r := rand.New(rand.NewSource(time.Now().UnixNano())) 76 | for i := 0; i < benchPolicySize; i++ { 77 | bits[i] = uint64(z.Next(r)) 78 | } 79 | return 80 | } 81 | 82 | func BenchmarkHas(b *testing.B) { 83 | a := benchAccess() 84 | b.Run("single", func(b *testing.B) { 85 | b.SetBytes(1) 86 | p := NewPolicy(benchPolicySize) 87 | for n := 0; n < b.N; n++ { 88 | p.Has(a[n%benchPolicySize]) 89 | } 90 | }) 91 | b.Run("concurrent", func(b *testing.B) { 92 | b.SetBytes(1) 93 | p := NewPolicy(benchPolicySize) 94 | b.RunParallel(func(pb *testing.PB) { 95 | for n := 0; pb.Next(); n++ { 96 | p.Has(a[n%benchPolicySize]) 97 | } 98 | }) 99 | }) 100 | } 101 | 102 | func BenchmarkHit(b *testing.B) { 103 | b.Run("single", func(b *testing.B) { 104 | b.SetBytes(1) 105 | p := NewPolicy(benchPolicySize) 106 | for n := 0; n < b.N; n++ { 107 | p.Hit(1) 108 | } 109 | }) 110 | b.Run("concurrent", func(b *testing.B) { 111 | b.SetBytes(1) 112 | p := NewPolicy(benchPolicySize) 113 | b.RunParallel(func(pb *testing.PB) { 114 | for pb.Next() { 115 | p.Hit(1) 116 | } 117 | }) 118 | }) 119 | } 120 | 121 | func BenchmarkClear(b *testing.B) { 122 | b.Run("single", func(b *testing.B) { 123 | b.SetBytes(1) 124 | p := NewPolicy(benchPolicySize) 125 | for n := 0; n < b.N; n++ { 126 | p.Del(1) 127 | } 128 | }) 129 | b.Run("concurrent", func(b *testing.B) { 130 | b.SetBytes(1) 131 | p := NewPolicy(benchPolicySize) 132 | b.RunParallel(func(pb *testing.PB) { 133 | for pb.Next() { 134 | p.Del(1) 135 | } 136 | }) 137 | }) 138 | } 139 | 140 | func BenchmarkEvict(b *testing.B) { 141 | b.Run("single", func(b *testing.B) { 142 | b.SetBytes(1) 143 | p := NewPolicy(benchPolicySize) 144 | for n := 0; n < b.N; n++ { 145 | p.Evict() 146 | } 147 | }) 148 | b.Run("concurrent", func(b *testing.B) { 149 | b.SetBytes(1) 150 | p := NewPolicy(benchPolicySize) 151 | b.RunParallel(func(pb *testing.PB) { 152 | for pb.Next() { 153 | p.Evict() 154 | } 155 | }) 156 | }) 157 | } 158 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # plru 2 | [](https://godoc.org/github.com/karlmcguire/plru) 3 | [](https://goreportcard.com/report/github.com/karlmcguire/plru) 4 | [](https://gocover.io/karlmcguire/plru) 5 | 6 | Pseudo-LRU implementation using 1-bit per entry and achieving hit ratios within 7 | 1-2% of Full-LRU (using expensive doubly-linked lists). This algorithm is 8 | commonly refered to as "PLRUm" because each bit serves as a MRU flag for each 9 | cache entry. 10 | 11 | Academic literature where PLRUm is mentioned: 12 | 13 | * [Performance evaluation of cache replacement policies for the SPEC CPU2000 benchmark suite](https://dl.acm.org/citation.cfm?id=986601) 14 | * Shows that PLRUm is a very good approximation of LRU 15 | * [Study of Different Cache Line Replacement Algorithms in Embedded Systems](https://people.kth.se/~ingo/MasterThesis/ThesisDamienGille2007.pdf) 16 | * Shows that PLRUm usually outperforms other PLRU algorithms 17 | * In some cases, PLRUm *outperforms* LRU 18 | 19 | **NOTE**: This is a small experiment repo and everything's still up in the air. 20 | Future plans include trying to implement a full cache out of this where it 21 | handles collisions and increases data locality. 22 | 23 | # usage 24 | 25 | This library is intended to be small and flexible for use within full cache 26 | implementations. Therefore, it is not safe for concurrent use out of the box and 27 | it does nothing to handle collisions. It makes sense to use this within a mutex 28 | lock close to a hashmap. 29 | 30 | ```go 31 | // create a new Policy tracking 1024 entries 32 | p := NewPolicy(1024) 33 | 34 | // on a Get operation, call policy.Hit() for the cache entry 35 | p.Hit(1) 36 | 37 | // when the cache is full, call policy.Evict() to get a LRU bit 38 | victim := p.Evict() 39 | 40 | // add some things to the victim location 41 | // ... 42 | 43 | // call policy.Hit() on the new entry to flag it as MRU 44 | p.Hit(victim) 45 | ``` 46 | 47 | # about 48 | 49 | This PLRUm implementation uses `uint64` blocks and uses round-robin for 50 | selecting which block to evict from (performs better than random selection). 51 | 52 | ## 1. empty state 53 | 54 | Before being populated, each block is 0. 55 | 56 |
57 |
58 |
68 |
69 |
80 |
81 |
90 |
91 |
100 |
101 |