Skip to content

Commit 2a85570

Browse files
tamirmsclaude
andcommitted
runcontainer: count or test before allocating in run-bitmap intersections
runContainer16.andBitmapContainer materialized the run as an 8 KiB bitmap container and then intersected, so an empty or array-sized result paid for a bitmap it never returned. It now counts the result with the existing andBitmapContainerCardinality and builds only that: an empty array container when the intersection is empty, an array of the right size when it is small, and the bitmap only when the result is one. A run with more than 64 intervals is ORed into a scratch bitmap on the stack, counted there, and extracted in one pass instead. bitmapContainer.iandRun16 did the same conversion on the argument side. It now counts the same way, returns a small result as an array built from the run's intervals, and otherwise clears the gaps between intervals in place; past 64 intervals the run is built in stack scratch and ANDed in one flat pass, since clearing gap by gap costs a call per interval. runContainer16.intersects computed rc.and(a) and tested the result for emptiness, allocating a container to answer a boolean; every mixed pairing with a run reached it through the array and bitmap dispatchers. It now walks the run's intervals against the other container, a masked word scan for bitmaps, a galloping merge for arrays or a probe per value when the array is much smaller than the interval list, an interval overlap test for runs, each returning at the first shared value. BenchmarkRunAndBitmap (added): And, in-place And and Intersects between a run-optimized bitmap and a dense one over a single key. Xeon 8375C: before after And, empty result 3.0 µs 8328 B/op 4 allocs 594 ns 104 B/op 2 allocs And, array result 8.3 µs 14504 B/op 8 allocs 4.6 µs 6280 B/op 6 allocs And, bitmap result 4.6 µs 16560 B/op 8 allocs 3.6 µs 8336 B/op 6 allocs And, 1024-interval run, array 11.9 µs 12456 B/op 8 allocs 10.0 µs 4232 B/op 6 allocs And, 1024-interval run, bitmap 9.5 µs 16560 B/op 8 allocs 7.6 µs 8336 B/op 6 allocs in-place And, bitmap result 2.4 µs 8224 B/op 2 allocs 290 ns 0 B/op 0 allocs in-place And, 1024-interval run 7.3 µs 8224 B/op 2 allocs 4.6 µs 0 B/op 0 allocs Intersects, disjoint 3.0 µs 8248 B/op 3 allocs 121 ns 0 B/op 0 allocs Intersects, overlapping 8.3 µs 14392 B/op 4 allocs 8 ns 0 B/op 0 allocs Intersects, few values, 1024 intervals 1.3 µs 32 B/op 2 allocs 62 ns 0 B/op 0 allocs Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 65fb6f8 commit 2a85570

4 files changed

Lines changed: 446 additions & 8 deletions

File tree

‎bitmapcontainer.go‎

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -798,8 +798,26 @@ func (bc *bitmapContainer) iand(a container) container {
798798
}
799799

800800
func (bc *bitmapContainer) iandRun16(rc *runContainer16) container {
801-
rcb := newBitmapContainerFromRun(rc)
802-
return bc.iandBitmap(rcb)
801+
if len(rc.iv) > runAndScratchIntervals {
802+
// Clearing gap by gap costs a call per interval; past this many the
803+
// run is built in scratch and ANDed in one flat pass instead.
804+
var scratch [bitmapContainerSize]uint64
805+
for i := range rc.iv {
806+
setBitmapRange(scratch[:], int(rc.iv[i].start), int(rc.iv[i].last())+1)
807+
}
808+
return bc.iandBitmap(&bitmapContainer{bitmap: scratch[:]})
809+
}
810+
card := rc.andBitmapContainerCardinality(bc)
811+
if card <= arrayDefaultMaxSize {
812+
answer := newArrayContainerCapacity(card)
813+
for i := range rc.iv {
814+
answer.content = appendBitmapRange(answer.content, bc.bitmap, int(rc.iv[i].start), int(rc.iv[i].last())+1)
815+
}
816+
return answer
817+
}
818+
clearBitmapGaps(bc.bitmap, rc.iv, 0, maxCapacity)
819+
bc.cardinality = card
820+
return bc
803821
}
804822

805823
func (bc *bitmapContainer) iandArray(ac *arrayContainer) container {
@@ -831,6 +849,26 @@ func (bc *bitmapContainer) andArrayCardinality(value2 *arrayContainer) int {
831849
return pos
832850
}
833851

852+
func (bc *bitmapContainer) intersectsRange(start, end uint) bool {
853+
if start >= end {
854+
return false
855+
}
856+
firstword, endword := start/64, (end-1)/64
857+
lo, hi := ^uint64(0)<<(start%64), ^uint64(0)>>((64-end)&63)
858+
if firstword == endword {
859+
return bc.bitmap[firstword]&lo&hi != 0
860+
}
861+
if bc.bitmap[firstword]&lo != 0 || bc.bitmap[endword]&hi != 0 {
862+
return true
863+
}
864+
for _, w := range bc.bitmap[firstword+1 : endword] {
865+
if w != 0 {
866+
return true
867+
}
868+
}
869+
return false
870+
}
871+
834872
func (bc *bitmapContainer) getCardinalityInRange(start, end uint) int {
835873
if start >= end {
836874
return 0

‎runbitmap_and_test.go‎

Lines changed: 260 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,260 @@
1+
package roaring
2+
3+
import (
4+
"math/rand"
5+
"testing"
6+
7+
"github.com/stretchr/testify/require"
8+
)
9+
10+
// randomRun builds a canonical run container: up to n sorted intervals of
11+
// at most maxLen values separated by gaps of at most maxGap, optionally
12+
// touching 0 and 65535.
13+
func randomRun(rng *rand.Rand, n, maxLen, maxGap int, touchEdges bool) *runContainer16 {
14+
rc := &runContainer16{}
15+
pos := 0
16+
if !touchEdges {
17+
pos = rng.Intn(100)
18+
}
19+
for i := 0; i < n && pos < 65536; i++ {
20+
length := min(1+rng.Intn(1+rng.Intn(maxLen)), 65536-pos)
21+
rc.iv = append(rc.iv, interval16{start: uint16(pos), length: uint16(length - 1)})
22+
pos += length + 1 + rng.Intn(maxGap)
23+
}
24+
if last := int(rc.maximum()); touchEdges && last != 65535 {
25+
if start := 65535 - rng.Intn(500); last+2 <= start {
26+
rc.iv = append(rc.iv, interval16{start: uint16(start), length: uint16(65535 - start)})
27+
}
28+
}
29+
return rc
30+
}
31+
32+
// randomBitmapContainer ANDs ands random words per position, so the density
33+
// is 1/2^ands.
34+
func randomBitmapContainer(rng *rand.Rand, ands int) *bitmapContainer {
35+
bc := newBitmapContainer()
36+
for i := range bc.bitmap {
37+
w := rng.Uint64()
38+
for k := 1; k < ands; k++ {
39+
w &= rng.Uint64()
40+
}
41+
bc.bitmap[i] = w
42+
}
43+
bc.computeCardinality()
44+
return bc
45+
}
46+
47+
func requireAndResult(t *testing.T, want, got container) {
48+
t.Helper()
49+
require.True(t, want.equals(got), "want %d values, got %d", want.getCardinality(), got.getCardinality())
50+
_, isArray := got.(*arrayContainer)
51+
require.Equal(t, got.getCardinality() <= arrayDefaultMaxSize, isArray, "container kind must follow its cardinality")
52+
}
53+
54+
// The reference is the old path: materialize the run, then the unchanged
55+
// bitmap-bitmap intersection.
56+
func TestRunAndBitmapContainer(t *testing.T) {
57+
rng := rand.New(rand.NewSource(1))
58+
for trial := 0; trial < 300; trial++ {
59+
rc := randomRun(rng, 1+rng.Intn(40), 3000, 2000, trial%5 == 0)
60+
bc := randomBitmapContainer(rng, 1+trial%4)
61+
want := newBitmapContainerFromRun(rc).andBitmap(bc)
62+
requireAndResult(t, want, rc.and(bc))
63+
requireAndResult(t, want, bc.and(rc))
64+
requireAndResult(t, want, bc.clone().iand(rc))
65+
}
66+
}
67+
68+
// Runs with more than runAndScratchIntervals intervals take the scratch
69+
// path; check it on regular and random runs against every density.
70+
func TestRunAndBitmapContainerManyIntervals(t *testing.T) {
71+
rng := rand.New(rand.NewSource(21))
72+
regular := func(stride, length int) *runContainer16 {
73+
rc := &runContainer16{}
74+
for start := 0; start+length <= 65536; start += stride {
75+
rc.iv = append(rc.iv, interval16{start: uint16(start), length: uint16(length - 1)})
76+
}
77+
return rc
78+
}
79+
runs := []*runContainer16{regular(32, 8), regular(64, 1), regular(3, 2), regular(200, 100)}
80+
for i := 0; i < 60; i++ {
81+
runs = append(runs, randomRun(rng, 65+rng.Intn(400), 60, 120, false))
82+
}
83+
for i, rc := range runs {
84+
require.Greater(t, len(rc.iv), runAndScratchIntervals, "run %d", i)
85+
for _, bc := range []*bitmapContainer{randomBitmapContainer(rng, 6), randomBitmapContainer(rng, 2), randomBitmapContainer(rng, 1), newBitmapContainerwithRange(0, 65535)} {
86+
want := newBitmapContainerFromRun(rc).andBitmap(bc)
87+
requireAndResult(t, want, rc.and(bc))
88+
requireAndResult(t, want, bc.and(rc))
89+
requireAndResult(t, want, bc.clone().iand(rc))
90+
require.Equal(t, !want.isEmpty(), rc.intersects(bc), "run %d", i)
91+
}
92+
}
93+
}
94+
95+
func TestRunAndBitmapContainerEdges(t *testing.T) {
96+
empty := newBitmapContainer()
97+
full := newBitmapContainerwithRange(0, 65535)
98+
single := &runContainer16{iv: []interval16{{start: 100, length: 9}}}
99+
edges := &runContainer16{iv: []interval16{{start: 0, length: 0}, {start: 65535, length: 0}}}
100+
atThreshold := &runContainer16{iv: []interval16{{start: 0, length: arrayDefaultMaxSize - 1}}}
101+
pastThreshold := &runContainer16{iv: []interval16{{start: 0, length: arrayDefaultMaxSize}}}
102+
for _, rc := range []*runContainer16{single, edges, atThreshold, pastThreshold} {
103+
for _, bc := range []*bitmapContainer{empty, full} {
104+
want := newBitmapContainerFromRun(rc).andBitmap(bc)
105+
requireAndResult(t, want, rc.and(bc))
106+
requireAndResult(t, want, bc.clone().iand(rc))
107+
}
108+
}
109+
}
110+
111+
// An empty intersection allocates only its empty array container; the old
112+
// path allocated an 8 KiB bitmap to hold the run first.
113+
func TestRunAndBitmapContainerEmptyAllocation(t *testing.T) {
114+
rc := &runContainer16{iv: []interval16{{start: 0, length: 999}, {start: 30000, length: 999}}}
115+
bc := newBitmapContainerwithRange(2000, 20000)
116+
require.LessOrEqual(t, testing.AllocsPerRun(100, func() {
117+
if !rc.and(bc).isEmpty() {
118+
t.Fatal("expected empty")
119+
}
120+
}), 1.0)
121+
// The clone costs two allocations, the empty result one.
122+
require.LessOrEqual(t, testing.AllocsPerRun(100, func() {
123+
_ = bc.clone().iand(rc)
124+
}), 3.0)
125+
}
126+
127+
// The reference is the old definition, !rc.and(c).isEmpty().
128+
func TestRunContainerIntersects(t *testing.T) {
129+
rng := rand.New(rand.NewSource(11))
130+
for trial := 0; trial < 400; trial++ {
131+
rc := randomRun(rng, 1+rng.Intn(30), 3000, 2000, trial%7 == 0)
132+
ac := newArrayContainer()
133+
for i := 0; i < rng.Intn(300); i++ {
134+
ac.iadd(uint16(rng.Intn(65536)))
135+
}
136+
others := []container{randomBitmapContainer(rng, 1+trial%3*3), ac, randomRun(rng, 1+rng.Intn(30), 3000, 2000, trial%5 == 0)}
137+
for _, c := range others {
138+
want := !rc.and(c).isEmpty()
139+
require.Equal(t, want, rc.intersects(c), "trial %d run vs %T", trial, c)
140+
require.Equal(t, want, c.intersects(rc), "trial %d %T vs run", trial, c)
141+
}
142+
}
143+
// A few values against many intervals take the search branch; the
144+
// reference is the same definition.
145+
many := randomRun(rng, 300, 60, 120, false)
146+
require.Greater(t, len(many.iv), 200)
147+
for trial := 0; trial < 200; trial++ {
148+
few := newArrayContainer()
149+
for i := 0; i < 1+rng.Intn(6); i++ {
150+
few.iadd(uint16(rng.Intn(65536)))
151+
}
152+
want := !many.and(few).isEmpty()
153+
require.Equal(t, want, many.intersects(few), "trial %d few vs many", trial)
154+
require.Equal(t, want, few.intersects(many), "trial %d many vs few", trial)
155+
}
156+
// Edges: empty array, touching intervals, single values at both ends.
157+
empty := newArrayContainer()
158+
edges := &runContainer16{iv: []interval16{{start: 0, length: 0}, {start: 65535, length: 0}}}
159+
require.False(t, edges.intersects(empty))
160+
one := newArrayContainer()
161+
one.iadd(65535)
162+
require.True(t, edges.intersects(one))
163+
touching := &runContainer16{iv: []interval16{{start: 1, length: 65533}}}
164+
require.False(t, edges.intersects(touching))
165+
require.True(t, edges.intersects(newBitmapContainerwithRange(0, 0)))
166+
}
167+
168+
func TestRunContainerIntersectsAllocatesNothing(t *testing.T) {
169+
rc := &runContainer16{iv: []interval16{{start: 0, length: 999}, {start: 30000, length: 999}}}
170+
bc := newBitmapContainerwithRange(2000, 20000)
171+
ac := newArrayContainerRange(2000, 3000)
172+
other := &runContainer16{iv: []interval16{{start: 5000, length: 100}}}
173+
for _, c := range []container{bc, ac, other} {
174+
require.Zero(t, testing.AllocsPerRun(100, func() { _ = rc.intersects(c) }), "%T", c)
175+
}
176+
}
177+
178+
// runBitmap holds one run container at key 0 covering n intervals of the
179+
// given length, spaced stride apart.
180+
func runBitmap(n, length, stride int) *Bitmap {
181+
bm := New()
182+
for i := 0; i < n; i++ {
183+
bm.AddRange(uint64(i*stride), uint64(i*stride+length))
184+
}
185+
bm.RunOptimize()
186+
return bm
187+
}
188+
189+
// BenchmarkRunAndBitmap intersects a run-optimized bitmap with a dense one
190+
// over a single key through the public API, one case per result kind, plus
191+
// the in-place form and the Intersects test.
192+
func BenchmarkRunAndBitmap(b *testing.B) {
193+
rng := rand.New(rand.NewSource(7))
194+
short := runBitmap(20, 300, 3000) // 6000 values
195+
long := runBitmap(4, 10000, 16000) // 40000 values
196+
many := runBitmap(1024, 8, 64) // 8192 values in 1024 intervals
197+
gaps, quarter, half, dense := New(), New(), New(), New()
198+
for v := uint32(0); v < 65536; v++ {
199+
if !short.Contains(v) && rng.Intn(2) == 0 {
200+
gaps.Add(v)
201+
}
202+
if rng.Intn(4) == 0 {
203+
quarter.Add(v)
204+
}
205+
if rng.Intn(2) == 0 {
206+
half.Add(v)
207+
}
208+
if rng.Intn(10) != 0 {
209+
dense.Add(v)
210+
}
211+
}
212+
for _, tc := range []struct {
213+
name string
214+
x, y *Bitmap
215+
}{
216+
{"and/empty", short, gaps},
217+
{"and/array", short, half},
218+
{"and/bitmap", long, dense},
219+
{"and/many-intervals/array", many, quarter},
220+
{"and/many-intervals/bitmap", many, dense},
221+
} {
222+
b.Run(tc.name, func(b *testing.B) {
223+
b.ReportAllocs()
224+
for b.Loop() {
225+
_ = And(tc.x, tc.y)
226+
}
227+
})
228+
}
229+
for _, tc := range []struct {
230+
name string
231+
x *Bitmap
232+
}{
233+
{"iand/bitmap", long},
234+
{"iand/many-intervals", many},
235+
} {
236+
b.Run(tc.name, func(b *testing.B) {
237+
b.ReportAllocs()
238+
x := dense.Clone()
239+
for b.Loop() {
240+
x.And(tc.x)
241+
}
242+
})
243+
}
244+
few := New()
245+
few.AddMany([]uint32{100, 30000, 60000})
246+
for _, tc := range []struct {
247+
name string
248+
x, y *Bitmap
249+
}{
250+
{"intersects/disjoint", short, gaps},
251+
{"intersects/overlapping", short, half},
252+
{"intersects/few-values-many-intervals", many, few},
253+
} {
254+
b.Run(tc.name, func(b *testing.B) {
255+
for b.Loop() {
256+
_ = tc.x.Intersects(tc.y)
257+
}
258+
})
259+
}
260+
}

0 commit comments

Comments
 (0)