Skip to content

Commit b871a51

Browse files
SimpleSeq.Seek: fast-path for count=1 case (#19101)
`SimpleSequence.search` is read on `erigon snapshots check-commitment-hist-at-blk-range` profiler ``` // Real data lengths: // - 70% len=1 // - 15% len=2 // - ... // // Real data return `idx`: // - 85% return idx=0 (first element) // - 10% return "not found" // - 5% other lengths // // As a result: early-check for `max` + full-scan search instead of `sort.Search` ``` Add fast-path for `len=1`: ``` │ old0.txt │ new.txt │ │ sec/op │ sec/op vs base │ SimpleSequenceSeek/n=1/hit_first-16 2.975n ± 0% 2.381n ± 0% -19.97% (p=0.000 n=10) SimpleSequenceSeek/n=1/hit_mid-16 2.970n ± 0% 2.387n ± 1% -19.63% (p=0.000 n=10) SimpleSequenceSeek/n=1/hit_last-16 2.999n ± 3% 2.391n ± 0% -20.29% (p=0.000 n=10) SimpleSequenceSeek/n=1/miss-16 2.772n ± 0% 2.371n ± 0% -14.48% (p=0.000 n=10) SimpleSequenceSeek/n=2/hit_first-16 3.570n ± 0% 2.405n ± 1% -32.62% (p=0.000 n=10) SimpleSequenceSeek/n=2/hit_mid-16 3.762n ± 0% 4.088n ± 0% +8.68% (p=0.000 n=10) SimpleSequenceSeek/n=2/hit_last-16 3.765n ± 0% 4.081n ± 0% +8.38% (p=0.000 n=10) SimpleSequenceSeek/n=2/miss-16 2.772n ± 0% 2.970n ± 0% +7.16% (p=0.000 n=10) SimpleSequenceSeek/n=4/hit_first-16 4.351n ± 0% 2.389n ± 1% -45.08% (p=0.000 n=10) SimpleSequenceSeek/n=4/hit_mid-16 3.768n ± 0% 4.037n ± 0% +7.15% (p=0.000 n=10) SimpleSequenceSeek/n=4/hit_last-16 3.766n ± 0% 4.010n ± 0% +6.48% (p=0.000 n=10) SimpleSequenceSeek/n=4/miss-16 3.364n ± 0% 3.748n ± 0% +11.40% (p=0.000 n=10) SimpleSequenceSeek/n=16/hit_first-16 6.281n ± 1% 2.394n ± 0% -61.89% (p=0.000 n=10) SimpleSequenceSeek/n=16/hit_mid-16 5.774n ± 0% 5.665n ± 0% -1.89% (p=0.000 n=10) SimpleSequenceSeek/n=16/hit_last-16 5.915n ± 1% 5.755n ± 0% -2.70% (p=0.000 n=10) SimpleSequenceSeek/n=16/miss-16 5.531n ± 0% 5.221n ± 0% -5.61% (p=0.000 n=10) geomean 3.871n 3.328n -14.01% │ old0.txt │ new.txt │ ``` ---- Brude-Force search (decided to not go for it): ``` SimpleSequenceSeek/n=4/hit_first-16 4.758n ± 0% 1.883n ± 2% -60.42% (p=0.000 n=10) SimpleSequenceSeek/n=4/hit_mid-16 4.000n ± 0% 2.865n ± 1% -28.36% (p=0.000 n=10) SimpleSequenceSeek/n=4/hit_last-16 4.163n ± 0% 3.523n ± 2% -15.38% (p=0.000 n=10) SimpleSequenceSeek/n=4/miss-16 1.991n ± 10% 1.321n ± 4% -33.64% (p=0.000 n=10) SimpleSequenceSeek/n=16/hit_first-16 6.138n ± 0% 1.889n ± 2% -69.22% (p=0.000 n=10) SimpleSequenceSeek/n=16/hit_mid-16 5.742n ± 0% 6.240n ± 2% +8.66% (p=0.000 n=10) SimpleSequenceSeek/n=16/hit_last-16 5.814n ± 0% 10.425n ± 3% +79.29% (p=0.000 n=10) SimpleSequenceSeek/n=16/miss-16 2.000n ± 9% 1.336n ± 8% -33.22% (p=0.000 n=10) ``` also i did take a look real data distribution on mainnet: ``` // Real data lengths: // - 70% len=1 // - 15% len=2 // - ... // // Real data return `idx`: // - 85% return idx=0 (first element) // - 10% return "not found" // - 5% other lengths // // As a result: early-check for `max` + full-scan search instead of `sort.Search` ```
1 parent 0e65d31 commit b871a51

File tree

2 files changed

+106
-23
lines changed

2 files changed

+106
-23
lines changed

db/recsplit/simpleseq/simple_sequence.go

Lines changed: 39 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,15 @@ type SimpleSequence struct {
1717
baseNum uint64
1818
raw []byte
1919
pos int
20+
count uint64 //u64-typed pre-calculated `len(raw)/4`
2021
}
2122

2223
func NewSimpleSequence(baseNum uint64, count uint64) *SimpleSequence {
2324
return &SimpleSequence{
2425
baseNum: baseNum,
2526
raw: make([]byte, count*4),
2627
pos: 0,
28+
count: count,
2729
}
2830
}
2931

@@ -37,60 +39,74 @@ func ReadSimpleSequence(baseNum uint64, raw []byte) *SimpleSequence {
3739
}
3840

3941
func (s *SimpleSequence) Get(i uint64) uint64 {
40-
idx := i * 4
41-
delta := binary.BigEndian.Uint32(s.raw[idx : idx+4])
42-
return s.baseNum + uint64(delta)
42+
delta := uint64(binary.BigEndian.Uint32(s.raw[i*4:]))
43+
return s.baseNum + delta
4344
}
4445

4546
func (s *SimpleSequence) Min() uint64 {
46-
return s.Get(0)
47+
delta := uint64(binary.BigEndian.Uint32(s.raw))
48+
return s.baseNum + delta
4749
}
4850

4951
func (s *SimpleSequence) Max() uint64 {
50-
return s.Get(s.Count() - 1)
52+
delta := uint64(binary.BigEndian.Uint32(s.raw[len(s.raw)-4:]))
53+
return s.baseNum + delta
5154
}
5255

5356
func (s *SimpleSequence) Count() uint64 {
54-
return uint64(len(s.raw) / 4)
57+
return s.count
5558
}
59+
func (s *SimpleSequence) Empty() bool { return len(s.raw) == 0 }
5660

5761
func (s *SimpleSequence) AddOffset(offset uint64) {
58-
binary.BigEndian.PutUint32(s.raw[s.pos*4:(s.pos+1)*4], uint32(offset-s.baseNum))
62+
binary.BigEndian.PutUint32(s.raw[s.pos*4:], uint32(offset-s.baseNum))
5963
s.pos++
6064
}
6165

6266
func (s *SimpleSequence) Reset(baseNum uint64, raw []byte) { // no `return parameter` to avoid heap-allocation of `s` object
6367
s.baseNum = baseNum
6468
s.raw = raw
6569
s.pos = len(raw) / 4
70+
s.count = uint64(len(raw) / 4)
6671
}
6772

6873
func (s *SimpleSequence) AppendBytes(buf []byte) []byte {
6974
return append(buf, s.raw...)
7075
}
7176

72-
func (s *SimpleSequence) search(v uint64) (int, bool) {
73-
c := s.Count()
74-
idx := sort.Search(int(c), func(i int) bool {
75-
return s.Get(uint64(i)) >= v
76-
})
77-
78-
if idx >= int(c) {
77+
func (s *SimpleSequence) search(seek uint64) (idx int, ok bool) {
78+
// Real data lengths:
79+
// - 70% len=1
80+
// - 15% len=2
81+
// - ...
82+
//
83+
// Real data return `idx`:
84+
// - 85% return idx=0 (first element)
85+
// - 10% return "not found"
86+
// - 5% other lengths
87+
if seek <= s.Min() { // fast-path for 1-st element hit
88+
return 0, true
89+
}
90+
if s.count == 1 { // if len=1 then nothing left to search
7991
return 0, false
8092
}
81-
return idx, true
82-
}
83-
84-
func (s *SimpleSequence) reverseSearch(v uint64) (int, bool) {
85-
c := s.Count()
86-
idx := sort.Search(int(c), func(i int) bool {
87-
return s.Get(c-uint64(i)-1) <= v
93+
idx = sort.Search(int(s.count), func(i int) bool {
94+
return s.Get(uint64(i)) >= seek
8895
})
96+
return idx, idx < int(s.count)
97+
}
8998

90-
if idx >= int(c) {
99+
func (s *SimpleSequence) reverseSearch(seek uint64) (idx int, ok bool) {
100+
if seek >= s.Max() { // fast-path for last element hit
101+
return int(s.count) - 1, true
102+
}
103+
if s.count == 1 { // if len=1 then nothing left to search
91104
return 0, false
92105
}
93-
return int(c) - idx - 1, true
106+
idx = sort.Search(int(s.count), func(i int) bool {
107+
return s.Get(uint64(i)) > seek
108+
}) - 1
109+
return idx, idx >= 0
94110
}
95111

96112
func (s *SimpleSequence) Seek(v uint64) (uint64, bool) {

db/recsplit/simpleseq/simple_sequence_test.go

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package simpleseq
22

33
import (
4+
"fmt"
45
"testing"
56

67
"github.com/stretchr/testify/require"
@@ -282,3 +283,69 @@ func TestSimpleSequence(t *testing.T) {
282283
require.Equal(t, uint64(0), v)
283284
})
284285
}
286+
287+
func TestReadSimpleSequence(t *testing.T) {
288+
// Build a sequence via NewSimpleSequence, serialize it, then deserialize via
289+
// ReadSimpleSequence (which goes through Reset). Regression test for a bug
290+
// where Reset did not update the cached `count` field, causing Count()==0 on
291+
// deserialized sequences.
292+
orig := NewSimpleSequence(1000, 4)
293+
orig.AddOffset(1001)
294+
orig.AddOffset(1007)
295+
orig.AddOffset(1015)
296+
orig.AddOffset(1027)
297+
298+
raw := orig.AppendBytes(nil)
299+
s := ReadSimpleSequence(1000, raw)
300+
301+
require.Equal(t, uint64(4), s.Count())
302+
require.Equal(t, uint64(1001), s.Min())
303+
require.Equal(t, uint64(1027), s.Max())
304+
305+
v, found := s.Seek(1007)
306+
require.True(t, found)
307+
require.Equal(t, uint64(1007), v)
308+
309+
v, found = s.Seek(9999)
310+
require.False(t, found)
311+
require.Equal(t, uint64(0), v)
312+
}
313+
314+
func makeSequence(n int) *SimpleSequence {
315+
base := uint64(1_000_000)
316+
s := NewSimpleSequence(base, uint64(n))
317+
for i := 0; i < n; i++ {
318+
s.AddOffset(base + uint64(i)*7 + 1)
319+
}
320+
return s
321+
}
322+
323+
func BenchmarkSimpleSequenceSeek(b *testing.B) {
324+
for _, size := range []int{1, 2, 4, 16} {
325+
s := makeSequence(size)
326+
minV := s.Min()
327+
maxV := s.Max()
328+
midV := s.Get(uint64(size / 2))
329+
330+
b.Run(fmt.Sprintf("n=%d/hit_first", size), func(b *testing.B) {
331+
for b.Loop() {
332+
s.Seek(minV)
333+
}
334+
})
335+
b.Run(fmt.Sprintf("n=%d/hit_mid", size), func(b *testing.B) {
336+
for b.Loop() {
337+
s.Seek(midV)
338+
}
339+
})
340+
b.Run(fmt.Sprintf("n=%d/hit_last", size), func(b *testing.B) {
341+
for b.Loop() {
342+
s.Seek(maxV)
343+
}
344+
})
345+
b.Run(fmt.Sprintf("n=%d/miss", size), func(b *testing.B) {
346+
for b.Loop() {
347+
s.Seek(maxV + 1)
348+
}
349+
})
350+
}
351+
}

0 commit comments

Comments
 (0)