github.com/cockroachdb/pebble@v0.0.0-20231214172447-ab4952c5f87b/internal/cache/robin_hood.go (about) 1 // Copyright 2020 The LevelDB-Go and Pebble Authors. All rights reserved. Use 2 // of this source code is governed by a BSD-style license that can be found in 3 // the LICENSE file. 4 5 package cache 6 7 import ( 8 "fmt" 9 "math/bits" 10 "os" 11 "runtime/debug" 12 "strings" 13 "time" 14 "unsafe" 15 16 "github.com/cockroachdb/pebble/internal/invariants" 17 "github.com/cockroachdb/pebble/internal/manual" 18 ) 19 20 var hashSeed = uint64(time.Now().UnixNano()) 21 22 // Fibonacci hash: https://probablydance.com/2018/06/16/fibonacci-hashing-the-optimization-that-the-world-forgot-or-a-better-alternative-to-integer-modulo/ 23 func robinHoodHash(k key, shift uint32) uint32 { 24 const m = 11400714819323198485 25 h := hashSeed 26 h ^= k.id * m 27 h ^= uint64(k.fileNum.FileNum()) * m 28 h ^= k.offset * m 29 return uint32(h >> shift) 30 } 31 32 type robinHoodEntry struct { 33 key key 34 // Note that value may point to a Go allocated object (if the "invariants" 35 // build tag was specified), even though the memory for the entry itself is 36 // manually managed. This is technically a volation of the Cgo pointer rules: 37 // 38 // https://golang.org/cmd/cgo/#hdr-Passing_pointers 39 // 40 // Specifically, Go pointers should not be stored in C allocated memory. The 41 // reason for this rule is that the Go GC will not look at C allocated memory 42 // to find pointers to Go objects. If the only reference to a Go object is 43 // stored in C allocated memory, the object will be reclaimed. What makes 44 // this "safe" is that the Cache guarantees that there are other pointers to 45 // the entry and shard which will keep them alive. In particular, every Go 46 // allocated entry in the cache is referenced by the shard.entries map. And 47 // every shard is referenced by the Cache.shards map. 48 value *entry 49 // The distance the entry is from its desired position. 50 dist uint32 51 } 52 53 type robinHoodEntries struct { 54 ptr unsafe.Pointer 55 len uint32 56 } 57 58 func newRobinHoodEntries(n uint32) robinHoodEntries { 59 size := uintptr(n) * unsafe.Sizeof(robinHoodEntry{}) 60 return robinHoodEntries{ 61 ptr: unsafe.Pointer(&(manual.New(int(size)))[0]), 62 len: n, 63 } 64 } 65 66 func (e robinHoodEntries) at(i uint32) *robinHoodEntry { 67 return (*robinHoodEntry)(unsafe.Pointer(uintptr(e.ptr) + 68 uintptr(i)*unsafe.Sizeof(robinHoodEntry{}))) 69 } 70 71 func (e robinHoodEntries) free() { 72 size := uintptr(e.len) * unsafe.Sizeof(robinHoodEntry{}) 73 buf := (*[manual.MaxArrayLen]byte)(e.ptr)[:size:size] 74 manual.Free(buf) 75 } 76 77 // robinHoodMap is an implementation of Robin Hood hashing. Robin Hood hashing 78 // is an open-address hash table using linear probing. The twist is that the 79 // linear probe distance is reduced by moving existing entries when inserting 80 // and deleting. This is accomplished by keeping track of how far an entry is 81 // from its "desired" slot (hash of key modulo number of slots). During 82 // insertion, if the new entry being inserted is farther from its desired slot 83 // than the target entry, we swap the target and new entry. This effectively 84 // steals from the "rich" target entry and gives to the "poor" new entry (thus 85 // the origin of the name). 86 // 87 // An extension over the base Robin Hood hashing idea comes from 88 // https://probablydance.com/2017/02/26/i-wrote-the-fastest-hashtable/. A cap 89 // is placed on the max distance an entry can be from its desired slot. When 90 // this threshold is reached during insertion, the size of the table is doubled 91 // and insertion is restarted. Additionally, the entries slice is given "max 92 // dist" extra entries on the end. The very last entry in the entries slice is 93 // never used and acts as a sentinel which terminates loops. The previous 94 // maxDist-1 entries act as the extra entries. For example, if the size of the 95 // table is 2, maxDist is computed as 4 and the actual size of the entry slice 96 // is 6. 97 // 98 // +---+---+---+---+---+---+ 99 // | 0 | 1 | 2 | 3 | 4 | 5 | 100 // +---+---+---+---+---+---+ 101 // ^ 102 // size 103 // 104 // In this scenario, the target entry for a key will always be in the range 105 // [0,1]. Valid entries may reside in the range [0,4] due to the linear probing 106 // of up to maxDist entries. The entry at index 5 will never contain a value, 107 // and instead acts as a sentinel (its distance is always 0). The max distance 108 // threshold is set to log2(num-entries). This ensures that retrieval is O(log 109 // N), though note that N is the number of total entries, not the count of 110 // valid entries. 111 // 112 // Deletion is implemented via the backward shift delete mechanism instead of 113 // tombstones. This preserves the performance of the table in the presence of 114 // deletions. See 115 // http://codecapsule.com/2013/11/17/robin-hood-hashing-backward-shift-deletion 116 // for details. 117 type robinHoodMap struct { 118 entries robinHoodEntries 119 size uint32 120 shift uint32 121 count uint32 122 maxDist uint32 123 } 124 125 func maxDistForSize(size uint32) uint32 { 126 desired := uint32(bits.Len32(size)) 127 if desired < 4 { 128 desired = 4 129 } 130 return desired 131 } 132 133 func newRobinHoodMap(initialCapacity int) *robinHoodMap { 134 m := &robinHoodMap{} 135 m.init(initialCapacity) 136 137 // Note: this is a no-op if invariants are disabled or race is enabled. 138 invariants.SetFinalizer(m, func(obj interface{}) { 139 m := obj.(*robinHoodMap) 140 if m.entries.ptr != nil { 141 fmt.Fprintf(os.Stderr, "%p: robin-hood map not freed\n", m) 142 os.Exit(1) 143 } 144 }) 145 return m 146 } 147 148 func (m *robinHoodMap) init(initialCapacity int) { 149 if initialCapacity < 1 { 150 initialCapacity = 1 151 } 152 targetSize := 1 << (uint(bits.Len(uint(2*initialCapacity-1))) - 1) 153 m.rehash(uint32(targetSize)) 154 } 155 156 func (m *robinHoodMap) free() { 157 if m.entries.ptr != nil { 158 m.entries.free() 159 m.entries.ptr = nil 160 } 161 } 162 163 func (m *robinHoodMap) rehash(size uint32) { 164 oldEntries := m.entries 165 166 m.size = size 167 m.shift = uint32(64 - bits.Len32(m.size-1)) 168 m.maxDist = maxDistForSize(size) 169 m.entries = newRobinHoodEntries(size + m.maxDist) 170 m.count = 0 171 172 for i := uint32(0); i < oldEntries.len; i++ { 173 e := oldEntries.at(i) 174 if e.value != nil { 175 m.Put(e.key, e.value) 176 } 177 } 178 179 if oldEntries.ptr != nil { 180 oldEntries.free() 181 } 182 } 183 184 // Find an entry containing the specified value. This is intended to be used 185 // from debug and test code. 186 func (m *robinHoodMap) findByValue(v *entry) *robinHoodEntry { 187 for i := uint32(0); i < m.entries.len; i++ { 188 e := m.entries.at(i) 189 if e.value == v { 190 return e 191 } 192 } 193 return nil 194 } 195 196 func (m *robinHoodMap) Count() int { 197 return int(m.count) 198 } 199 200 func (m *robinHoodMap) Put(k key, v *entry) { 201 maybeExists := true 202 n := robinHoodEntry{key: k, value: v, dist: 0} 203 for i := robinHoodHash(k, m.shift); ; i++ { 204 e := m.entries.at(i) 205 if maybeExists && k == e.key { 206 // Entry already exists: overwrite. 207 e.value = n.value 208 m.checkEntry(i) 209 return 210 } 211 212 if e.value == nil { 213 // Found an empty entry: insert here. 214 *e = n 215 m.count++ 216 m.checkEntry(i) 217 return 218 } 219 220 if e.dist < n.dist { 221 // Swap the new entry with the current entry because the current is 222 // rich. We then continue to loop, looking for a new location for the 223 // current entry. Note that this is also the not-found condition for 224 // retrieval, which means that "k" is not present in the map. See Get(). 225 n, *e = *e, n 226 m.checkEntry(i) 227 maybeExists = false 228 } 229 230 // The new entry gradually moves away from its ideal position. 231 n.dist++ 232 233 // If we've reached the max distance threshold, grow the table and restart 234 // the insertion. 235 if n.dist == m.maxDist { 236 m.rehash(2 * m.size) 237 i = robinHoodHash(n.key, m.shift) - 1 238 n.dist = 0 239 maybeExists = false 240 } 241 } 242 } 243 244 func (m *robinHoodMap) Get(k key) *entry { 245 var dist uint32 246 for i := robinHoodHash(k, m.shift); ; i++ { 247 e := m.entries.at(i) 248 if k == e.key { 249 // Found. 250 return e.value 251 } 252 if e.dist < dist { 253 // Not found. 254 return nil 255 } 256 dist++ 257 } 258 } 259 260 func (m *robinHoodMap) Delete(k key) { 261 var dist uint32 262 for i := robinHoodHash(k, m.shift); ; i++ { 263 e := m.entries.at(i) 264 if k == e.key { 265 m.checkEntry(i) 266 // We found the entry to delete. Shift the following entries backwards 267 // until the next empty value or entry with a zero distance. Note that 268 // empty values are guaranteed to have "dist == 0". 269 m.count-- 270 for j := i + 1; ; j++ { 271 t := m.entries.at(j) 272 if t.dist == 0 { 273 *e = robinHoodEntry{} 274 return 275 } 276 e.key = t.key 277 e.value = t.value 278 e.dist = t.dist - 1 279 e = t 280 m.checkEntry(j) 281 } 282 } 283 if dist > e.dist { 284 // Not found. 285 return 286 } 287 dist++ 288 } 289 } 290 291 func (m *robinHoodMap) checkEntry(i uint32) { 292 if invariants.Enabled { 293 e := m.entries.at(i) 294 if e.value != nil { 295 pos := robinHoodHash(e.key, m.shift) 296 if (uint32(i) - pos) != e.dist { 297 fmt.Fprintf(os.Stderr, "%d: invalid dist=%d, expected %d: %s\n%s", 298 i, e.dist, uint32(i)-pos, e.key, debug.Stack()) 299 os.Exit(1) 300 } 301 if e.dist > m.maxDist { 302 fmt.Fprintf(os.Stderr, "%d: invalid dist=%d > maxDist=%d: %s\n%s", 303 i, e.dist, m.maxDist, e.key, debug.Stack()) 304 os.Exit(1) 305 } 306 } 307 } 308 } 309 310 func (m *robinHoodMap) String() string { 311 var buf strings.Builder 312 fmt.Fprintf(&buf, "count: %d\n", m.count) 313 for i := uint32(0); i < m.entries.len; i++ { 314 e := m.entries.at(i) 315 if e.value != nil { 316 fmt.Fprintf(&buf, "%d: [%s,%p,%d]\n", i, e.key, e.value, e.dist) 317 } 318 } 319 return buf.String() 320 }