├── README ├── clockpro.go ├── clockpro_test.go └── testdata └── domains.txt /README: -------------------------------------------------------------------------------- 1 | go-clockpro: the CLOCK-Pro cache eviction algorithm 2 | 3 | godoc: http://godoc.org/github.com/dgryski/go-clockpro 4 | -------------------------------------------------------------------------------- /clockpro.go: -------------------------------------------------------------------------------- 1 | // Package clockpro implements the CLOCK-Pro caching algorithm. 2 | /* 3 | 4 | CLOCK-Pro is a patent-free alternative to the Adaptive Replacement Cache, 5 | https://en.wikipedia.org/wiki/Adaptive_replacement_cache . 6 | It is an approximation of LIRS ( https://en.wikipedia.org/wiki/LIRS_caching_algorithm ), 7 | much like the CLOCK page replacement algorithm is an approximation of LRU. 8 | 9 | This implementation is based on the python code from https://bitbucket.org/SamiLehtinen/pyclockpro . 10 | 11 | Slides describing the algorithm: http://fr.slideshare.net/huliang64/clockpro 12 | 13 | The original paper: http://static.usenix.org/event/usenix05/tech/general/full_papers/jiang/jiang_html/html.html 14 | 15 | It is MIT licensed, like the original. 16 | */ 17 | package clockpro 18 | 19 | import "container/ring" 20 | 21 | type pageType int 22 | 23 | const ( 24 | ptTest pageType = iota 25 | ptCold 26 | ptHot 27 | ) 28 | 29 | func (p pageType) String() string { 30 | 31 | switch p { 32 | case ptTest: 33 | return "Test" 34 | case ptCold: 35 | return "Cold" 36 | case ptHot: 37 | return "Hot" 38 | } 39 | 40 | return "unknown" 41 | } 42 | 43 | type entry struct { 44 | ptype pageType 45 | key string 46 | val interface{} 47 | ref bool 48 | } 49 | 50 | type Cache struct { 51 | mem_max int 52 | mem_cold int 53 | keys map[string]*ring.Ring 54 | 55 | hand_hot *ring.Ring 56 | hand_cold *ring.Ring 57 | hand_test *ring.Ring 58 | 59 | count_hot int 60 | count_cold int 61 | count_test int 62 | } 63 | 64 | func New(size int) *Cache { 65 | return &Cache{ 66 | mem_max: size, 67 | mem_cold: size, 68 | keys: make(map[string]*ring.Ring), 69 | } 70 | } 71 | 72 | func (c *Cache) Get(key string) interface{} { 73 | 74 | r := c.keys[key] 75 | 76 | if r == nil { 77 | return nil 78 | } 79 | 80 | mentry := r.Value.(*entry) 81 | 82 | if mentry.val == nil { 83 | return nil 84 | } 85 | 86 | mentry.ref = true 87 | return mentry.val 88 | } 89 | 90 | func (c *Cache) Set(key string, value interface{}) { 91 | 92 | r := c.keys[key] 93 | 94 | if r == nil { 95 | // no cache entry? add it 96 | r = &ring.Ring{Value: &entry{ref: false, val: value, ptype: ptCold, key: key}} 97 | c.meta_add(key, r) 98 | c.count_cold++ 99 | return 100 | } 101 | 102 | mentry := r.Value.(*entry) 103 | 104 | if mentry.val != nil { 105 | // cache entry was a hot or cold page 106 | mentry.val = value 107 | mentry.ref = true 108 | return 109 | } 110 | 111 | // cache entry was a test page 112 | if c.mem_cold < c.mem_max { 113 | c.mem_cold++ 114 | } 115 | mentry.ref = false 116 | mentry.val = value 117 | mentry.ptype = ptHot 118 | c.count_test-- 119 | c.meta_del(r) 120 | c.meta_add(key, r) 121 | c.count_hot++ 122 | } 123 | 124 | func (c *Cache) meta_add(key string, r *ring.Ring) { 125 | 126 | c.evict() 127 | 128 | c.keys[key] = r 129 | r.Link(c.hand_hot) 130 | 131 | if c.hand_hot == nil { 132 | // first element 133 | c.hand_hot = r 134 | c.hand_cold = r 135 | c.hand_test = r 136 | } 137 | 138 | if c.hand_cold == c.hand_hot { 139 | c.hand_cold = c.hand_cold.Prev() 140 | } 141 | } 142 | 143 | func (c *Cache) meta_del(r *ring.Ring) { 144 | 145 | delete(c.keys, r.Value.(*entry).key) 146 | 147 | if r == c.hand_hot { 148 | c.hand_hot = c.hand_hot.Prev() 149 | } 150 | 151 | if r == c.hand_cold { 152 | c.hand_cold = c.hand_cold.Prev() 153 | } 154 | 155 | if r == c.hand_test { 156 | c.hand_test = c.hand_test.Prev() 157 | } 158 | 159 | r.Prev().Unlink(1) 160 | } 161 | 162 | func (c *Cache) evict() { 163 | 164 | for c.mem_max <= c.count_hot+c.count_cold { 165 | c.run_hand_cold() 166 | } 167 | } 168 | 169 | func (c *Cache) run_hand_cold() { 170 | 171 | mentry := c.hand_cold.Value.(*entry) 172 | 173 | if mentry.ptype == ptCold { 174 | 175 | if mentry.ref { 176 | mentry.ptype = ptHot 177 | mentry.ref = false 178 | c.count_cold-- 179 | c.count_hot++ 180 | } else { 181 | mentry.ptype = ptTest 182 | mentry.val = nil 183 | c.count_cold-- 184 | c.count_test++ 185 | for c.mem_max < c.count_test { 186 | c.run_hand_test() 187 | } 188 | } 189 | } 190 | 191 | c.hand_cold = c.hand_cold.Next() 192 | 193 | for c.mem_max-c.mem_cold < c.count_hot { 194 | c.run_hand_hot() 195 | } 196 | } 197 | 198 | func (c *Cache) run_hand_hot() { 199 | 200 | if c.hand_hot == c.hand_test { 201 | c.run_hand_test() 202 | } 203 | 204 | mentry := c.hand_hot.Value.(*entry) 205 | 206 | if mentry.ptype == ptHot { 207 | 208 | if mentry.ref { 209 | mentry.ref = false 210 | } else { 211 | mentry.ptype = ptCold 212 | c.count_hot-- 213 | c.count_cold++ 214 | } 215 | } 216 | 217 | c.hand_hot = c.hand_hot.Next() 218 | } 219 | 220 | func (c *Cache) run_hand_test() { 221 | 222 | if c.hand_test == c.hand_cold { 223 | c.run_hand_cold() 224 | } 225 | 226 | mentry := c.hand_test.Value.(*entry) 227 | 228 | if mentry.ptype == ptTest { 229 | 230 | prev := c.hand_test.Prev() 231 | c.meta_del(c.hand_test) 232 | c.hand_test = prev 233 | 234 | c.count_test-- 235 | if c.mem_cold > 1 { 236 | c.mem_cold-- 237 | } 238 | } 239 | 240 | c.hand_test = c.hand_test.Next() 241 | } 242 | 243 | func (c *Cache) dump() string { 244 | 245 | var b []byte 246 | 247 | var end *ring.Ring = nil 248 | for elt := c.hand_hot; elt != end; elt = elt.Next() { 249 | end = c.hand_hot 250 | m := elt.Value.(*entry) 251 | 252 | if c.hand_hot == elt { 253 | b = append(b, '2') 254 | } 255 | 256 | if c.hand_test == elt { 257 | b = append(b, '0') 258 | } 259 | 260 | if c.hand_cold == elt { 261 | b = append(b, '1') 262 | } 263 | 264 | switch m.ptype { 265 | case ptHot: 266 | if m.ref { 267 | b = append(b, 'H') 268 | } else { 269 | b = append(b, 'h') 270 | } 271 | case ptCold: 272 | if m.ref { 273 | b = append(b, 'C') 274 | } else { 275 | b = append(b, 'c') 276 | } 277 | case ptTest: 278 | b = append(b, 'n') 279 | } 280 | } 281 | 282 | return string(b) 283 | } 284 | -------------------------------------------------------------------------------- /clockpro_test.go: -------------------------------------------------------------------------------- 1 | package clockpro 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "os" 7 | "testing" 8 | ) 9 | 10 | func TestCache(t *testing.T) { 11 | 12 | // Test data was generated from the python code 13 | f, err := os.Open("testdata/domains.txt") 14 | 15 | if err != nil { 16 | t.Fatal(err) 17 | } 18 | 19 | scanner := bufio.NewScanner(f) 20 | 21 | cache := New(200) 22 | 23 | for scanner.Scan() { 24 | fields := bytes.Fields(scanner.Bytes()) 25 | 26 | key := string(fields[0]) 27 | wantHit := fields[1][0] == 'h' 28 | 29 | var hit bool 30 | v := cache.Get(key) 31 | if v == nil { 32 | cache.Set(key, key) 33 | } else { 34 | hit = true 35 | if v.(string) != key { 36 | t.Errorf("cache returned bad data: got %+v , want %+v\n", v, key) 37 | } 38 | } 39 | if hit != wantHit { 40 | t.Errorf("cache hit mismatch: got %v, want %v\n", hit, wantHit) 41 | } 42 | } 43 | } 44 | --------------------------------------------------------------------------------