From 65f28ab64019651034eca58893054b2015718bdb Mon Sep 17 00:00:00 2001 From: Evgenii Guguchkin Date: Thu, 5 Nov 2026 12:27:47 +0300 Subject: [PATCH 1/3] perf: galloping search in static iterator NextGeq --- node/node_static.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/node/node_static.go b/node/node_static.go index d3b15408..b10909ae 100644 --- a/node/node_static.go +++ b/node/node_static.go @@ -2,7 +2,6 @@ package node import ( "math" - "sort" ) type staticCursor struct { @@ -54,7 +53,7 @@ func (n *staticAsc) NextGeq(nextID LID) LID { } from := n.ptr - idx := sort.Search(len(n.data)-from, func(i int) bool { return n.data[from+i] >= nextID.Unpack() }) + idx := searchGeqGallopAsc(n.data[from:], nextID.Unpack()) if idx >= len(n.data)-from { return NullLID() } @@ -79,7 +78,7 @@ func (n *staticDesc) NextGeq(nextID LID) LID { if n.ptr < 0 { return NullLID() } - idx := sort.Search(n.ptr+1, func(i int) bool { return n.data[i] > nextID.Unpack() }) - 1 + idx := searchGtGallopDesc(n.data[:n.ptr+1], nextID.Unpack()) - 1 if idx < 0 { return NullLID() } From 7e6e554c7f551a2371200ec99b5cd4f9137cd321 Mon Sep 17 00:00:00 2001 From: Evgenii Guguchkin Date: Thu, 5 Nov 2026 12:54:08 +0300 Subject: [PATCH 2/3] add benches --- node/bench_test.go | 60 +++++++++++++++++++++++++++++++++++++--------- 1 file changed, 49 insertions(+), 11 deletions(-) diff --git a/node/bench_test.go b/node/bench_test.go index 0eac3f96..382e83ee 100644 --- a/node/bench_test.go +++ b/node/bench_test.go @@ -2,6 +2,7 @@ package node import ( "fmt" + "math" "math/rand" "testing" @@ -95,21 +96,56 @@ func BenchmarkOr(b *testing.B) { } } +// BenchmarkAnd traverses AND of two static asc leaves (the production zigzag +// in node_and.go) over LID sets of different density. density=random keeps the +// two independent random lists; otherwise the right list is every k-th value +// of the left one, so intersections are guaranteed and the zigzag skip is ~k +// elements: dense = lockstep, sparse = rare long skips. A fresh node is built +// per iteration since iterators are single-pass. func BenchmarkAnd(b *testing.B) { sizes := []int{1000, 10_000, 1_000_000} + densities := []struct { + name string + k func(size int) int + }{ + {"random", nil}, + {"dense", func(size int) int { return 2 }}, + {"medium", func(size int) int { return int(math.Sqrt(float64(size))) }}, + {"sparse", func(size int) int { return size / 64 }}, + } for _, s := range sizes { - b.Run(fmt.Sprintf("size=%d", s), func(b *testing.B) { - r := rand.New(rand.NewSource(benchRandSeed)) - res := make([]uint32, 0, s) - n := NewAnd(newNodeStaticSize(r, s), newNodeStaticSize(r, s)) - - for b.Loop() { - res = readAllInto(n, res) - } - - assert.Equal(b, cap(res), s) - }) + for _, d := range densities { + b.Run(fmt.Sprintf("size=%d/density=%s", s, d.name), func(b *testing.B) { + r := rand.New(rand.NewSource(benchRandSeed)) + dataA, _ := Generate(r, s) + var dataB []uint32 + if d.k != nil { + k := d.k(s) + dataB = make([]uint32, 0, s/k+1) + for i := 0; i < len(dataA); i += k { + dataB = append(dataB, dataA[i]) + } + } else { + dataB, _ = Generate(r, s) + } + + // control run: the intersection must be exactly dataB + want := readAllIntoGeq(NewAnd(NewStatic(dataA, true), NewStatic(dataB, true)), nil) + if d.k != nil { + assert.Equal(b, len(dataB), len(want)) + } + + res := make([]uint32, 0, s) + for b.Loop() { + n := NewAnd(NewStatic(dataA, true), NewStatic(dataB, true)) + res = readAllInto(n, res[:0]) + } + assert.Equal(b, len(want), len(res)) + benchSink = res[len(res)/2] + b.ReportMetric(float64(b.Elapsed().Nanoseconds())/float64(b.N*len(want)), "ns/match") + }) + } } } @@ -237,3 +273,5 @@ func BenchmarkComplex(b *testing.B) { }) } } + +var benchSink uint32 From eedad0fcf8e429c3e796895ff3ea10a5a91179ab Mon Sep 17 00:00:00 2001 From: Evgenii Guguchkin Date: Fri, 6 Nov 2026 14:07:07 +0300 Subject: [PATCH 3/3] =?UTF-8?q?fix=20comment=20and=20remove=20legacy=20ben?= =?UTF-8?q?ch=20tri=D1=81k?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- node/bench_test.go | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/node/bench_test.go b/node/bench_test.go index 382e83ee..877f2338 100644 --- a/node/bench_test.go +++ b/node/bench_test.go @@ -96,12 +96,7 @@ func BenchmarkOr(b *testing.B) { } } -// BenchmarkAnd traverses AND of two static asc leaves (the production zigzag -// in node_and.go) over LID sets of different density. density=random keeps the -// two independent random lists; otherwise the right list is every k-th value -// of the left one, so intersections are guaranteed and the zigzag skip is ~k -// elements: dense = lockstep, sparse = rare long skips. A fresh node is built -// per iteration since iterators are single-pass. +// BenchmarkAnd traverses AND of two static asc leaves over LID sets of different density func BenchmarkAnd(b *testing.B) { sizes := []int{1000, 10_000, 1_000_000} densities := []struct { @@ -142,7 +137,6 @@ func BenchmarkAnd(b *testing.B) { res = readAllInto(n, res[:0]) } assert.Equal(b, len(want), len(res)) - benchSink = res[len(res)/2] b.ReportMetric(float64(b.Elapsed().Nanoseconds())/float64(b.N*len(want)), "ns/match") }) } @@ -273,5 +267,3 @@ func BenchmarkComplex(b *testing.B) { }) } } - -var benchSink uint32