Skip to content

Commit b0b7042

Browse files
committed
part: Optimize findIndex for node48
Use a 1-based indexing in node48.index so that 0 always means that the key is not present. Check for exact match first for node48 in findIndex, then check if key is within bounds and only then binary search. Implement binary search directly instead of using sort.Search to avoid the function closure. goos: darwin goarch: arm64 pkg: github.com/cilium/statedb/part cpu: Apple M1 Pro │ a.txt │ b.txt │ │ sec/op │ sec/op vs base │ _Uint64Map_Sequential_Insert-8 430.5µ ± ∞ ¹ 476.4µ ± ∞ ¹ ~ (p=0.095 n=5) _Uint64Map_Sequential_Txn_Insert-8 69.15µ ± ∞ ¹ 68.59µ ± ∞ ¹ ~ (p=0.151 n=5) _Uint64Map_Random_Insert-8 519.9µ ± ∞ ¹ 541.2µ ± ∞ ¹ ~ (p=0.421 n=5) _Uint64Map_Random_Txn_Insert-8 107.1µ ± ∞ ¹ 104.0µ ± ∞ ¹ -2.87% (p=0.008 n=5) _Insert_RootOnlyWatch-8 72.25µ ± ∞ ¹ 69.49µ ± ∞ ¹ ~ (p=0.056 n=5) _Insert-8 100.18µ ± ∞ ¹ 95.70µ ± ∞ ¹ -4.47% (p=0.016 n=5) _GetInsert-8 79.07µ ± ∞ ¹ 78.45µ ± ∞ ¹ ~ (p=0.151 n=5) _Hashmap_Insert-8 36.09µ ± ∞ ¹ 36.22µ ± ∞ ¹ ~ (p=0.841 n=5) _Delete_Random-8 10.188m ± ∞ ¹ 8.640m ± ∞ ¹ -15.19% (p=0.008 n=5) _find48-8 2.038n ± ∞ ¹ 2.065n ± ∞ ¹ ~ (p=0.095 n=5) _findIndex48_hit-8 10.180n ± ∞ ¹ 2.047n ± ∞ ¹ -79.89% (p=0.008 n=5) _findIndex48_miss-8 8.468n ± ∞ ¹ 2.260n ± ∞ ¹ -73.31% (p=0.008 n=5) │ a.txt │ b.txt │ │ objects/sec │ objects/sec vs base │ _Insert_RootOnlyWatch-8 13.84M ± ∞ ¹ 14.39M ± ∞ ¹ ~ (p=0.056 n=5) _Insert-8 9.982M ± ∞ ¹ 10.449M ± ∞ ¹ +4.68% (p=0.016 n=5) _GetInsert-8 12.65M ± ∞ ¹ 12.75M ± ∞ ¹ ~ (p=0.151 n=5) _Hashmap_Insert-8 27.71M ± ∞ ¹ 27.61M ± ∞ ¹ ~ (p=0.841 n=5) _Delete_Random-8 9.816M ± ∞ ¹ 11.573M ± ∞ ¹ +17.91% (p=0.008 n=5) Signed-off-by: Jussi Maki <jussi.maki@isovalent.com>
1 parent e56c0c8 commit b0b7042

File tree

3 files changed

+50
-39
lines changed

3 files changed

+50
-39
lines changed

part/node.go

Lines changed: 36 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ package part
66
import (
77
"bytes"
88
"fmt"
9-
"sort"
109
"strings"
1110
"unsafe"
1211
)
@@ -246,7 +245,7 @@ func (n *header[T]) promote(txnID uint64) *header[T] {
246245
node48.leaf = n.getLeaf()
247246
copy(node48.children[:], node16.children[:node16.size()])
248247
for i, k := range node16.keys[:node16.size()] {
249-
node48.index[k] = int8(i)
248+
node48.index[k] = uint8(i + 1)
250249
}
251250
if n.watch != nil {
252251
node48.watch = make(chan struct{})
@@ -374,16 +373,34 @@ func (n *header[T]) findIndex(key byte) (*header[T], int) {
374373
}
375374
return nil, size
376375
case nodeKind48:
377-
children := n.children()
378-
idx := sort.Search(len(children), func(i int) bool {
379-
return children[i].prefix()[0] >= key
380-
})
381-
if idx >= n.size() || children[idx].prefix()[0] != key {
382-
// No node found, return nil and the index into
383-
// which it should go.
384-
return nil, idx
376+
n48 := n.node48()
377+
// Check for exact match first
378+
if idx := n48.index[key]; idx != 0 {
379+
i := int(idx - 1)
380+
return n48.children[i], i
381+
}
382+
// No exact match. Binary search to find insertion point.
383+
size := n48.size()
384+
children := n48.children[:size]
385+
// Is the key between smallest and highest?
386+
switch {
387+
case key < children[0].key():
388+
return nil, 0
389+
case key > children[size-1].key():
390+
return nil, size
385391
}
386-
return children[idx], idx
392+
// No exact match, but key is in range. Binary search to find insertion point.
393+
// We're not using sort.Search as that requires a function closure.
394+
lo, hi := 0, size
395+
for lo < hi {
396+
mid := int(uint(lo+hi) / 2)
397+
if children[mid].key() < key {
398+
lo = mid + 1
399+
} else {
400+
hi = mid
401+
}
402+
}
403+
return nil, lo
387404
case nodeKind256:
388405
return n.node256().children[key], int(key)
389406
default:
@@ -418,10 +435,10 @@ func (n *header[T]) find(key byte) *header[T] {
418435
case nodeKind48:
419436
n48 := n.node48()
420437
idx := n48.index[key]
421-
if idx < 0 {
438+
if idx == 0 {
422439
return nil
423440
}
424-
return n48.children[idx]
441+
return n48.children[idx-1]
425442
case nodeKind256:
426443
return n.node256().children[key]
427444
default:
@@ -452,11 +469,11 @@ func (n *header[T]) insert(idx int, child *header[T]) {
452469
n48 := n.node48()
453470
for i := size - 1; i >= idx; i-- {
454471
c := n48.children[i]
455-
n48.index[c.key()] = int8(i + 1)
472+
n48.index[c.key()] = uint8(i + 2)
456473
n48.children[i+1] = c
457474
}
458475
n48.children[idx] = child
459-
n48.index[child.key()] = int8(idx)
476+
n48.index[child.key()] = uint8(idx + 1)
460477
case nodeKind256:
461478
n.node256().children[child.key()] = child
462479
default:
@@ -489,9 +506,9 @@ func (n *header[T]) remove(idx int) {
489506
for i := idx; i < newSize; i++ {
490507
child := children[i+1]
491508
children[i] = child
492-
n48.index[child.key()] = int8(i)
509+
n48.index[child.key()] = uint8(i + 1)
493510
}
494-
n48.index[key] = -1
511+
n48.index[key] = 0
495512
children[newSize] = nil
496513
case nodeKind256:
497514
n.node256().children[idx] = nil
@@ -547,8 +564,8 @@ type node48[T any] struct {
547564
header[T]
548565
txnID uint64 // transaction ID that last mutated this node
549566
children [48]*header[T]
550-
leaf *leaf[T] // non-nil if this node contains a value
551-
index [256]int8
567+
leaf *leaf[T] // non-nil if this node contains a value
568+
index [256]uint8 // 1-based index for key in [children] (0 is absent, 1 is children[0])
552569
}
553570

554571
type node256[T any] struct {

part/part_test.go

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1667,20 +1667,17 @@ func Benchmark_find48(b *testing.B) {
16671667
header: header[bool]{},
16681668
children: [48]*header[bool]{},
16691669
leaf: nil,
1670-
index: [256]int8{},
1670+
index: [256]uint8{},
16711671
}
16721672
keyBytes := [48]byte{}
16731673
leaves := [48]leaf[bool]{}
1674-
for i := range n.index {
1675-
n.index[i] = -1
1676-
}
16771674
for i := range 48 {
16781675
keyBytes[i] = byte(i)
16791676
leaves[i].prefixP = &keyBytes[i]
16801677
leaves[i].prefixLen = 1
16811678
leaves[i].setKind(nodeKindLeaf)
16821679
n.children[i] = leaves[i].self()
1683-
n.index[byte(i)] = int8(i)
1680+
n.index[byte(i)] = uint8(i + 1)
16841681
}
16851682
n.setKind(nodeKind48)
16861683
n.setSize(48)
@@ -1695,7 +1692,7 @@ func Benchmark_findIndex48_hit(b *testing.B) {
16951692
header: header[bool]{},
16961693
children: [48]*header[bool]{},
16971694
leaf: nil,
1698-
index: [256]int8{},
1695+
index: [256]uint8{},
16991696
}
17001697
keyBytes := [48]byte{}
17011698
leaves := [48]leaf[bool]{}
@@ -1705,7 +1702,7 @@ func Benchmark_findIndex48_hit(b *testing.B) {
17051702
leaves[i].prefixLen = 1
17061703
leaves[i].setKind(nodeKindLeaf)
17071704
n.children[i] = leaves[i].self()
1708-
n.index[byte(i)] = int8(i + 1)
1705+
n.index[byte(i)] = uint8(i + 1)
17091706
}
17101707
n.setKind(nodeKind48)
17111708
n.setSize(48)
@@ -1720,20 +1717,17 @@ func Benchmark_findIndex48_miss(b *testing.B) {
17201717
header: header[bool]{},
17211718
children: [48]*header[bool]{},
17221719
leaf: nil,
1723-
index: [256]int8{},
1720+
index: [256]uint8{},
17241721
}
17251722
keyBytes := [48]byte{}
17261723
leaves := [48]leaf[bool]{}
1727-
for i := range n.index {
1728-
n.index[i] = -1
1729-
}
17301724
for i := range 48 {
17311725
keyBytes[i] = byte(i)
17321726
leaves[i].prefixP = &keyBytes[i]
17331727
leaves[i].prefixLen = 1
17341728
leaves[i].setKind(nodeKindLeaf)
17351729
n.children[i] = leaves[i].self()
1736-
n.index[byte(i)] = int8(i)
1730+
n.index[byte(i)] = uint8(i + 1)
17371731
}
17381732
n.setKind(nodeKind48)
17391733
n.setSize(48)

part/txn.go

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -596,15 +596,15 @@ func (txn *Txn[T]) removeChild(parent *header[T], index int) (newParent *header[
596596
demoted.setKind(nodeKind48)
597597
demoted.setSize(size - 1)
598598
demoted.setTxnID(txn.txnID)
599-
n48 := demoted.node48()
600-
n48.leaf = parent.getLeaf()
601-
children := n48.children[:0]
602-
for k, n := range parent.node256().children[:] {
603-
if k != index && n != nil {
604-
n48.index[k] = int8(len(children))
605-
children = append(children, n)
599+
n48 := demoted.node48()
600+
n48.leaf = parent.getLeaf()
601+
children := n48.children[:0]
602+
for k, n := range parent.node256().children[:] {
603+
if k != index && n != nil {
604+
n48.index[k] = uint8(len(children) + 1)
605+
children = append(children, n)
606+
}
606607
}
607-
}
608608
newParent = demoted
609609
case parent.kind() == nodeKind48 && size <= 17:
610610
demoted := (&node16[T]{header: *parent}).self()

0 commit comments

Comments
 (0)