Skip to content

Commit f47d186

Browse files
committed
internal/cache: add robinHoodMap
Add `robinHoodMap` which is a hash map implemented using Robin Hood hashing. `robinHoodMap` is specialized to use `key` and `*entry` as the key and value for the map respectively. The memory for the backing array is manually allocated. This lowers GC pressure by moving the allocated memory out of the Go heap, and by moving all of `*entry` pointers out of the Go heap. Use `robinHoodMap` for both the `shard.blocks` and `shard.files` maps.
1 parent e7c41b8 commit f47d186

File tree

5 files changed

+467
-31
lines changed

5 files changed

+467
-31
lines changed

internal/cache/clockpro.go

Lines changed: 69 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@ package cache // import "github.com/cockroachdb/pebble/internal/cache"
1919

2020
import (
2121
"fmt"
22+
"os"
2223
"runtime"
24+
"runtime/debug"
2325
"sync"
2426
"sync/atomic"
2527

@@ -37,6 +39,17 @@ type key struct {
3739
offset uint64
3840
}
3941

42+
// file returns the "file key" for the receiver. This is the key used for the
43+
// shard.files map.
44+
func (k key) file() key {
45+
k.offset = 0
46+
return k
47+
}
48+
49+
func (k key) String() string {
50+
return fmt.Sprintf("%d/%d/%d", k.id, k.fileNum, k.offset)
51+
}
52+
4053
// Handle provides a strong reference to an entry in the cache. The reference
4154
// does not pin the entry in the cache, but it does prevent the underlying byte
4255
// slice from being reused. When entry is non-nil, value is initialized to
@@ -107,8 +120,17 @@ type shard struct {
107120
reservedSize int64
108121
maxSize int64
109122
coldTarget int64
110-
blocks map[key]*entry // fileNum+offset -> block
111-
files map[fileKey]*entry // fileNum -> list of blocks
123+
blocks robinHoodMap // fileNum+offset -> block
124+
files robinHoodMap // fileNum -> list of blocks
125+
126+
// The blocks and files maps store values in manually managed memory that is
127+
// invisible to the Go GC. This is fine for Value and entry objects that are
128+
// stored in manually managed memory. Auto Values and the associated auto
129+
// entries need to have a reference that the Go GC is aware of to prevent
130+
// them from being reclaimed. The entries map provides this reference. When
131+
// the "invariants" build tag is set, all Value and entry objects are Go
132+
// allocated and the entries map will contain a reference to every entry.
133+
entries map[*entry]struct{}
112134

113135
handHot *entry
114136
handCold *entry
@@ -121,7 +143,7 @@ type shard struct {
121143

122144
func (c *shard) Get(id, fileNum, offset uint64) Handle {
123145
c.mu.RLock()
124-
e := c.blocks[key{fileKey{id, fileNum}, offset}]
146+
e := c.blocks.Get(key{fileKey{id, fileNum}, offset})
125147
var value *Value
126148
if e != nil {
127149
value = e.getValue()
@@ -160,7 +182,7 @@ func (c *shard) Set(id, fileNum, offset uint64, value *Value) Handle {
160182
defer c.mu.Unlock()
161183

162184
k := key{fileKey{id, fileNum}, offset}
163-
e := c.blocks[k]
185+
e := c.blocks.Get(k)
164186
if e != nil && e.manual != value.manual() {
165187
panic(fmt.Sprintf("pebble: inconsistent caching of manual Value: entry=%t vs value=%t",
166188
e.manual, value.manual()))
@@ -231,7 +253,7 @@ func (c *shard) Delete(id, fileNum, offset uint64) {
231253
c.mu.Lock()
232254
defer c.mu.Unlock()
233255

234-
e := c.blocks[key{fileKey{id, fileNum}, offset}]
256+
e := c.blocks.Get(key{fileKey{id, fileNum}, offset})
235257
if e == nil {
236258
return
237259
}
@@ -243,7 +265,8 @@ func (c *shard) EvictFile(id, fileNum uint64) {
243265
c.mu.Lock()
244266
defer c.mu.Unlock()
245267

246-
blocks := c.files[fileKey{id, fileNum}]
268+
fkey := key{fileKey{id, fileNum}, 0}
269+
blocks := c.files.Get(fkey)
247270
if blocks == nil {
248271
return
249272
}
@@ -291,7 +314,12 @@ func (c *shard) metaAdd(key key, e *entry) bool {
291314
return false
292315
}
293316

294-
c.blocks[key] = e
317+
c.blocks.Put(key, e)
318+
if !e.managed {
319+
// Go allocated entries need to be referenced from Go memory. The entries
320+
// map provides that reference.
321+
c.entries[e] = struct{}{}
322+
}
295323

296324
if c.handHot == nil {
297325
// first element
@@ -306,8 +334,9 @@ func (c *shard) metaAdd(key key, e *entry) bool {
306334
c.handCold = c.handCold.prev()
307335
}
308336

309-
if fileBlocks := c.files[key.fileKey]; fileBlocks == nil {
310-
c.files[key.fileKey] = e
337+
fkey := key.file()
338+
if fileBlocks := c.files.Get(fkey); fileBlocks == nil {
339+
c.files.Put(fkey, e)
311340
} else {
312341
fileBlocks.linkFile(e)
313342
}
@@ -323,7 +352,12 @@ func (c *shard) metaDel(e *entry) {
323352
}
324353
e.setValue(nil)
325354

326-
delete(c.blocks, e.key)
355+
c.blocks.Delete(e.key)
356+
if !e.managed {
357+
// Go allocated entries need to be referenced from Go memory. The entries
358+
// map provides that reference.
359+
delete(c.entries, e)
360+
}
327361

328362
if e == c.handHot {
329363
c.handHot = c.handHot.prev()
@@ -342,10 +376,11 @@ func (c *shard) metaDel(e *entry) {
342376
c.handTest = nil
343377
}
344378

379+
fkey := e.key.file()
345380
if next := e.unlinkFile(); e == next {
346-
delete(c.files, e.key.fileKey)
381+
c.files.Delete(fkey)
347382
} else {
348-
c.files[e.key.fileKey] = next
383+
c.files.Put(fkey, next)
349384
}
350385

351386
c.metaCheck(e)
@@ -354,21 +389,28 @@ func (c *shard) metaDel(e *entry) {
354389
// Check that the specified entry is not referenced by the cache.
355390
func (c *shard) metaCheck(e *entry) {
356391
if invariants.Enabled {
357-
for _, t := range c.blocks {
358-
if e == t {
359-
panic("not reached")
360-
}
392+
if _, ok := c.entries[e]; ok {
393+
fmt.Fprintf(os.Stderr, "%p: %s unexpectedly found in entries map\n%s",
394+
e, e.key, debug.Stack())
395+
os.Exit(1)
361396
}
362-
for _, t := range c.files {
363-
if e == t {
364-
panic("not reached")
365-
}
397+
if c.blocks.findByValue(e) != nil {
398+
fmt.Fprintf(os.Stderr, "%p: %s unexpectedly found in blocks map\n%s\n%s",
399+
e, e.key, &c.blocks, debug.Stack())
400+
os.Exit(1)
401+
}
402+
if c.files.findByValue(e) != nil {
403+
fmt.Fprintf(os.Stderr, "%p: %s unexpectedly found in files map\n%s\n%s",
404+
e, e.key, &c.files, debug.Stack())
405+
os.Exit(1)
366406
}
367407
// NB: c.hand{Hot,Cold,Test} are pointers into a single linked list. We
368408
// only have to traverse one of them to check all of them.
369409
for t := c.handHot.next(); t != c.handHot; t = t.next() {
370410
if e == t {
371-
panic("not reached")
411+
fmt.Fprintf(os.Stderr, "%p: %s unexpectedly found in blocks list\n%s",
412+
e, e.key, debug.Stack())
413+
os.Exit(1)
372414
}
373415
}
374416
}
@@ -553,6 +595,8 @@ func clearCache(obj interface{}) {
553595
s.mu.Lock()
554596
s.maxSize = 0
555597
s.evict()
598+
s.blocks.free()
599+
s.files.free()
556600
s.mu.Unlock()
557601
}
558602
}
@@ -567,9 +611,10 @@ func newShards(size int64, shards int) *Cache {
567611
c.shards[i] = shard{
568612
maxSize: size / int64(len(c.shards)),
569613
coldTarget: size / int64(len(c.shards)),
570-
blocks: make(map[key]*entry),
571-
files: make(map[fileKey]*entry),
614+
entries: make(map[*entry]struct{}),
572615
}
616+
c.shards[i].blocks.init(16)
617+
c.shards[i].files.init(16)
573618
}
574619
// TODO(peter): This finalizer is used to clear the cache when the Cache
575620
// itself is GC'd. Investigate making this explicit, and then changing the
@@ -703,7 +748,7 @@ func (c *Cache) Metrics() Metrics {
703748
for i := range c.shards {
704749
s := &c.shards[i]
705750
s.mu.RLock()
706-
m.Count += int64(len(s.blocks))
751+
m.Count += int64(s.blocks.Count())
707752
m.Size += s.sizeHot + s.sizeCold
708753
s.mu.RUnlock()
709754
m.Hits += atomic.LoadInt64(&s.hits)

internal/cache/entry.go

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -59,9 +59,13 @@ type entry struct {
5959
}
6060
size int64
6161
ptype entryType
62-
// Is the memory for the entry manually managed? A manually managed entry can
63-
// only store manually managed values (Value.manual() is true).
62+
// Can the entry hold a manual Value? Only a manually managed entry can store
63+
// manually managed values (Value.manual() is true).
6464
manual bool
65+
// Was the entry allocated using the Go allocator or the manual
66+
// allocator. This can differ from the setting of the manual field due when
67+
// the "invariants" build tag is set.
68+
managed bool
6569
// referenced is atomically set to indicate that this entry has been accessed
6670
// since the last time one of the clock hands swept it.
6771
referenced int32
@@ -78,11 +82,12 @@ func newEntry(s *shard, key key, size int64, manual bool) *entry {
7882
e = &entry{}
7983
}
8084
*e = entry{
81-
key: key,
82-
size: size,
83-
ptype: etCold,
84-
manual: manual,
85-
shard: s,
85+
key: key,
86+
size: size,
87+
ptype: etCold,
88+
manual: manual,
89+
managed: e.managed,
90+
shard: s,
8691
}
8792
e.blockLink.next = e
8893
e.blockLink.prev = e

internal/cache/entry_normal.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ func entryAllocNew() *entry {
2424
a := entryAllocPool.Get().(*entryAllocCache)
2525
e := a.alloc()
2626
entryAllocPool.Put(a)
27+
e.managed = true
2728
return e
2829
}
2930

0 commit comments

Comments
 (0)