Skip to content

Commit cdbff48

Browse files
committed
Union run containers by run instead of by value
runContainer16.inplaceUnion added the other container one value at a time. It now inserts up to 16 runs in place and merges above that. toArrayContainer allocates once, iorRun16 merges above 4 runs, and the union and intersect helpers check the next run before searching.
1 parent 142f290 commit cdbff48

4 files changed

Lines changed: 266 additions & 11 deletions

File tree

‎arraycontainer.go‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -428,12 +428,20 @@ func (ac *arrayContainer) iorBitmap(bc2 *bitmapContainer) container {
428428
return bc1
429429
}
430430

431+
// arrayRunInsertMaxRuns is the most runs iorRun16 inserts into the array one
432+
// at a time. Each insert moves the tail of the array, so past a few runs one
433+
// merge is usually cheaper.
434+
const arrayRunInsertMaxRuns = 4
435+
431436
func (ac *arrayContainer) iorRun16(rc *runContainer16) container {
432437
runCardinality := rc.getCardinality()
433438
// heuristic for if the container should maybe be an
434439
// array container.
435440
if runCardinality < ac.getCardinality() &&
436441
runCardinality+ac.getCardinality() < arrayDefaultMaxSize {
442+
if len(rc.iv) > arrayRunInsertMaxRuns {
443+
return ac.iorArray(rc.toArrayContainer())
444+
}
437445
var result container
438446
result = ac
439447
for _, run := range rc.iv {

‎real_data_benchmark_test.go‎

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,3 +186,37 @@ func BenchmarkRealDataFastOr(b *testing.B) {
186186
return FastOr(bitmaps...).GetCardinality()
187187
})
188188
}
189+
190+
func BenchmarkRealDataOrInPlace(b *testing.B) {
191+
benchmarkRealDataAggregate(b, func(bitmaps []*Bitmap) uint64 {
192+
tot := uint64(0)
193+
for i := 1; i < len(bitmaps); i++ {
194+
x := bitmaps[i-1].Clone()
195+
x.Or(bitmaps[i])
196+
tot += x.GetCardinality()
197+
}
198+
return tot
199+
})
200+
}
201+
202+
func BenchmarkRealDataOrFold(b *testing.B) {
203+
benchmarkRealDataAggregate(b, func(bitmaps []*Bitmap) uint64 {
204+
x := bitmaps[0].Clone()
205+
for _, y := range bitmaps[1:] {
206+
x.Or(y)
207+
}
208+
return x.GetCardinality()
209+
})
210+
}
211+
212+
func BenchmarkRealDataAndInPlace(b *testing.B) {
213+
benchmarkRealDataAggregate(b, func(bitmaps []*Bitmap) uint64 {
214+
tot := uint64(0)
215+
for i := 1; i < len(bitmaps); i++ {
216+
x := bitmaps[i-1].Clone()
217+
x.And(bitmaps[i])
218+
tot += x.GetCardinality()
219+
}
220+
return tot
221+
})
222+
}

‎runcontainer.go‎

Lines changed: 63 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -417,12 +417,15 @@ func (rc *runContainer16) union(b *runContainer16) *runContainer16 {
417417
} else {
418418
// !mergedUsed
419419
if !canMerge16(cura, curb) {
420+
// copy every run that ends before the other side's next run
420421
if cura.start < curb.start {
421-
m = append(m, cura)
422-
na++
422+
next := rc.indexOfIntervalAtOrAfter(int(curb.start)-1, na+1)
423+
m = append(m, rc.iv[na:next]...)
424+
na = next
423425
} else {
424-
m = append(m, curb)
425-
nb++
426+
next := b.indexOfIntervalAtOrAfter(int(cura.start)-1, nb+1)
427+
m = append(m, b.iv[nb:next]...)
428+
nb = next
426429
}
427430
} else {
428431
merged = mergeInterval16s(cura, curb)
@@ -595,6 +598,11 @@ func (rc *runContainer16) unionCardinality(b *runContainer16) uint {
595598

596599
// indexOfIntervalAtOrAfter is a helper for union.
597600
func (rc *runContainer16) indexOfIntervalAtOrAfter(key int, startIndex int) int {
601+
// union only asks for keys past rc.iv[startIndex-1], so a key at or
602+
// before the end of rc.iv[startIndex] needs no search.
603+
if startIndex < len(rc.iv) && int(rc.iv[startIndex].last()) >= key {
604+
return startIndex
605+
}
598606
w, already, _ := rc.searchRange(key, startIndex, 0)
599607
if already {
600608
return w
@@ -1418,6 +1426,9 @@ func intersectWithLeftover16(astart, alast, bstart, blast int) (isOverlap, isLef
14181426
}
14191427

14201428
func (rc *runContainer16) findNextIntervalThatIntersectsStartingFrom(startIndex int, key int) (index int, done bool) {
1429+
if startIndex+1 < len(rc.iv) && int(rc.iv[startIndex+1].start) > key {
1430+
return startIndex, false
1431+
}
14211432
w, _, _ := rc.searchRange(key, startIndex, 0)
14221433
// rc.search always returns w < len(rc.iv)
14231434
if w < startIndex {
@@ -2267,16 +2278,53 @@ func (rc *runContainer16) ior(a container) container {
22672278
panic("unsupported container type")
22682279
}
22692280

2281+
// runInsertMaxRuns is the most runs inplaceUnion inserts one at a time. Each
2282+
// insert shifts the runs after it, so past a few a merge is usually cheaper.
2283+
const runInsertMaxRuns = 16
2284+
22702285
func (rc *runContainer16) inplaceUnion(rc2 *runContainer16) container {
2271-
for _, p := range rc2.iv {
2272-
last := int(p.last())
2273-
for i := int(p.start); i <= last; i++ {
2274-
rc.Add(uint16(i))
2286+
if len(rc2.iv) <= runInsertMaxRuns {
2287+
for _, p := range rc2.iv {
2288+
rc.addInterval(p)
22752289
}
2290+
} else {
2291+
*rc = *rc.union(rc2)
22762292
}
22772293
return rc.toEfficientContainer()
22782294
}
22792295

2296+
// addInterval adds the values of iv in place, merging the runs it touches.
2297+
func (rc *runContainer16) addInterval(iv interval16) {
2298+
start, last := int(iv.start), int(iv.last())
2299+
// runs i..j-1 overlap or touch iv
2300+
i, j := 0, len(rc.iv)
2301+
for i < j {
2302+
h := int(uint(i+j) >> 1)
2303+
if int(rc.iv[h].last())+1 < start {
2304+
i = h + 1
2305+
} else {
2306+
j = h
2307+
}
2308+
}
2309+
j = len(rc.iv)
2310+
for lo := i; lo < j; {
2311+
h := int(uint(lo+j) >> 1)
2312+
if int(rc.iv[h].start) <= last+1 {
2313+
lo = h + 1
2314+
} else {
2315+
j = h
2316+
}
2317+
}
2318+
if i == j {
2319+
rc.iv = slices.Insert(rc.iv, i, iv)
2320+
return
2321+
}
2322+
start = min(start, int(rc.iv[i].start))
2323+
last = max(last, int(rc.iv[j-1].last()))
2324+
rc.iv[i] = newInterval16Range(uint16(start), uint16(last))
2325+
rc.iv = append(rc.iv[:i+1], rc.iv[j:]...)
2326+
}
2327+
22802328
// Such code should not be used as it will not preserve the container invariants:
22812329
//func (rc *runContainer16) iorBitmapContainer(bc *bitmapContainer) container {
22822330
// it := bc.getShortIterator()
@@ -2717,9 +2765,13 @@ func (rc *runContainer16) toEfficientContainerFromCardinality(card int) containe
27172765
}
27182766

27192767
func (rc *runContainer16) toArrayContainer() *arrayContainer {
2720-
ac := newArrayContainer()
2721-
for i := range rc.iv {
2722-
ac.iaddRange(int(rc.iv[i].start), int(rc.iv[i].last())+1)
2768+
ac := newArrayContainerSize(rc.getCardinality())
2769+
pos := 0
2770+
for _, iv := range rc.iv {
2771+
for v := int(iv.start); v <= int(iv.last()); v++ {
2772+
ac.content[pos] = uint16(v)
2773+
pos++
2774+
}
27232775
}
27242776
return ac
27252777
}

‎runcontainer_union_test.go‎

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
package roaring
2+
3+
import (
4+
"fmt"
5+
"math/rand"
6+
"slices"
7+
"testing"
8+
9+
"github.com/stretchr/testify/require"
10+
)
11+
12+
// unionValues is the reference: the sorted union of the values in a and b.
13+
func unionValues(a, b []uint16) []uint16 {
14+
out := slices.Concat(a, b)
15+
slices.Sort(out)
16+
return slices.Compact(out)
17+
}
18+
19+
func containerValues(c container) []uint16 {
20+
var out []uint16
21+
for it := c.getShortIterator(); it.hasNext(); {
22+
out = append(out, it.next())
23+
}
24+
return out
25+
}
26+
27+
// randomRuns returns a run container of up to maxRuns runs starting at or
28+
// after lo, each 1..maxLen long, separated by gaps of 1..maxGap.
29+
func randomRuns(r *rand.Rand, lo, maxRuns, maxLen, maxGap int) *runContainer16 {
30+
var vals []uint16
31+
v := lo
32+
for k := 0; k < maxRuns && v < 1<<16; k++ {
33+
for end := min(v+1+r.Intn(maxLen), 1<<16); v < end; v++ {
34+
vals = append(vals, uint16(v))
35+
}
36+
v += 1 + r.Intn(maxGap)
37+
}
38+
return newRunContainer16FromVals(true, vals...)
39+
}
40+
41+
func TestRunContainerUnionExhaustive(t *testing.T) {
42+
// every pair of subsets of an 8-value window, at both ends of the range
43+
for _, base := range []int{0, 1<<16 - 8} {
44+
for x := 0; x < 256; x++ {
45+
for y := 0; y < 256; y++ {
46+
var av, bv []uint16
47+
for i := 0; i < 8; i++ {
48+
if x&(1<<i) != 0 {
49+
av = append(av, uint16(base+i))
50+
}
51+
if y&(1<<i) != 0 {
52+
bv = append(bv, uint16(base+i))
53+
}
54+
}
55+
a, b := newRunContainer16FromVals(true, av...), newRunContainer16FromVals(true, bv...)
56+
want := unionValues(av, bv)
57+
require.Equal(t, want, containerValues(a.union(b)))
58+
var both []uint16
59+
for _, v := range av {
60+
if slices.Contains(bv, v) {
61+
both = append(both, v)
62+
}
63+
}
64+
require.Equal(t, both, containerValues(a.intersect(b)))
65+
got := a.Clone().inplaceUnion(b)
66+
require.Equal(t, want, containerValues(got))
67+
if len(want) > 0 {
68+
require.NoError(t, got.validate())
69+
}
70+
}
71+
}
72+
}
73+
}
74+
75+
func TestRunContainerInplaceUnionRandom(t *testing.T) {
76+
r := rand.New(rand.NewSource(1))
77+
for trial := 0; trial < 500; trial++ {
78+
maxLen, maxGap := 1+r.Intn(100), 1+r.Intn(100)
79+
a := randomRuns(r, r.Intn(64), 1+r.Intn(3000), maxLen, maxGap)
80+
// small operands straddle runInsertMaxRuns; every other trial uses a large one
81+
b := randomRuns(r, r.Intn(1<<16), 1+r.Intn(2*runInsertMaxRuns), maxLen, maxGap)
82+
if trial%2 == 1 {
83+
b = randomRuns(r, r.Intn(64), 1+r.Intn(3000), maxLen, maxGap)
84+
}
85+
av, bv := containerValues(a), containerValues(b)
86+
87+
got := a.inplaceUnion(b)
88+
require.Equal(t, unionValues(av, bv), containerValues(got))
89+
require.NoError(t, got.validate())
90+
require.Equal(t, bv, containerValues(b), "operand modified")
91+
}
92+
// self-union
93+
a := randomRuns(r, 0, 10, 50, 50)
94+
want := containerValues(a)
95+
require.Equal(t, want, containerValues(a.inplaceUnion(a)))
96+
}
97+
98+
func TestArrayContainerIorRun16Random(t *testing.T) {
99+
r := rand.New(rand.NewSource(2))
100+
for trial := 0; trial < 2000; trial++ {
101+
ac := newArrayContainer()
102+
for n := 1 + r.Intn(arrayDefaultMaxSize); n > 0; n-- {
103+
ac.iadd(uint16(r.Intn(1 << 16)))
104+
}
105+
rc := randomRuns(r, r.Intn(1<<16), 1+r.Intn(4*arrayRunInsertMaxRuns), 1+r.Intn(20), 1+r.Intn(2000))
106+
acv, rcv := containerValues(ac), containerValues(rc)
107+
108+
got := ac.iorRun16(rc)
109+
require.Equal(t, unionValues(acv, rcv), containerValues(got))
110+
require.Equal(t, rcv, containerValues(rc), "operand modified")
111+
if len(rcv) < len(acv) && len(rcv)+len(acv) < arrayDefaultMaxSize {
112+
require.Same(t, ac, got)
113+
}
114+
}
115+
}
116+
117+
func TestRunContainerToArrayContainer(t *testing.T) {
118+
r := rand.New(rand.NewSource(3))
119+
for trial := 0; trial < 500; trial++ {
120+
rc := randomRuns(r, r.Intn(1000), 1+r.Intn(200), 1+r.Intn(40), 1+r.Intn(300))
121+
require.Equal(t, containerValues(rc), rc.toArrayContainer().content)
122+
}
123+
}
124+
125+
func BenchmarkRunContainerInplaceUnion(b *testing.B) {
126+
receiver := newRunContainer16()
127+
for v := 0; v < 1<<16; v += 64 {
128+
receiver.iv = append(receiver.iv, newInterval16Range(uint16(v), uint16(v+31)))
129+
}
130+
for _, runs := range []int{4, 1024} {
131+
operand := newRunContainer16()
132+
for i := 0; i < runs; i++ {
133+
v := i * (1 << 16) / runs
134+
operand.iv = append(operand.iv, newInterval16Range(uint16(v+40), uint16(v+47)))
135+
}
136+
b.Run(fmt.Sprintf("runs=%d", runs), func(b *testing.B) {
137+
for i := 0; i < b.N; i++ {
138+
receiver.Clone().inplaceUnion(operand)
139+
}
140+
})
141+
}
142+
}
143+
144+
func BenchmarkArrayContainerIorRun16(b *testing.B) {
145+
ac := newArrayContainer()
146+
for v := 0; v < 1<<16; v += 32 {
147+
ac.content = append(ac.content, uint16(v))
148+
}
149+
for _, runs := range []int{4, 100} {
150+
rc := newRunContainer16()
151+
for i := 0; i < runs; i++ {
152+
v := i*(1<<16)/runs + 7
153+
rc.iv = append(rc.iv, newInterval16Range(uint16(v), uint16(v+2)))
154+
}
155+
b.Run(fmt.Sprintf("runs=%d", runs), func(b *testing.B) {
156+
for i := 0; i < b.N; i++ {
157+
ac.clone().(*arrayContainer).iorRun16(rc)
158+
}
159+
})
160+
}
161+
}

0 commit comments

Comments
 (0)