Skip to content

Avoid materializing unions in OrCardinality - #563

Open
gitRasheed wants to merge 2 commits into
RoaringBitmap:masterfrom
gitRasheed:perf/or-cardinality
Open

Avoid materializing unions in OrCardinality#563
gitRasheed wants to merge 2 commits into
RoaringBitmap:masterfrom
gitRasheed:perf/or-cardinality

Conversation

@gitRasheed

@gitRasheed gitRasheed commented Sep 5, 2026

Copy link
Copy Markdown

Description

OrCardinality built the union container for each matching key pair and read its cardinality. It now calls the count-only container methods, present since 2017 (#40, #46) but never called. The array-array case becomes len(a) + len(b) - intersection2by2Cardinality(a, b), which already gallops on skewed sizes.

Type of Change

  • Performance improvement
  • Test improvements

Changes Made

What was changed?

  • roaring.go, roaring64/roaring64.go: OrCardinality calls orCardinality on the containers instead of or(...).getCardinality().
  • setutil.go: union2by2Cardinality uses the formula above; the merge loop is gone.
  • Tests for cardinality, input immutability and zero allocations across container pairings, empty inputs and unequal keys, in both packages. Benchmarks for arrays, container pairs and real datasets.

Why was it changed?

Materializing the union allocated up to 21 MB per pass over a dataset for a number the existing methods compute in place.

How was it changed?

Two call sites and one function body. No new algorithm, assembly or API.

Testing

go test ./... passes on x86-64 and on c7g.xlarge, c8g.xlarge and c9g.xlarge. A differential test against Or().GetCardinality() on 38,000 random bitmap pairs across all container types, with and without RunOptimize, found no mismatch.

Formatting

go fmt, make unconvert and git diff --check clean.

Fuzzing

smat fuzzer, 300 seconds: 553,072 executions, no failures. The smat model checks OrCardinality against its reference.

Performance Impact

BenchmarkRealDataOrCardinality runs all 12 real datasets, as loaded and after RunOptimize, on c7g.xlarge, c8g.xlarge and c9g.xlarge: ten one-second samples per case, Go 1.26.2, one pinned CPU, variant order balanced. One op is a pass over a dataset's adjacent bitmap pairs. Time reduction against master, geometric mean over datasets:

Instance Less time, as loaded Less time, after RunOptimize
c7g.xlarge 64.8% 79.7%
c8g.xlarge 72.2% 82.8%
c9g.xlarge 68.6% 82.0%

All 72 instance/dataset/mode cases are faster at p < 0.001, and all allocate nothing where master allocated up to 21 MB per op. The smallest gain is uscensus2000, which has almost no array containers. The largest is census1881 after RunOptimize, 5.32 ms to 32.2 µs, where master built 19 MB of temporary containers per pass.

c8g.xlarge, every dataset

Medians in µs/op. The last two columns are what master allocated per op; this branch allocates nothing.

Dataset Mode Master µs/op This PR µs/op Less time Master B/op Master allocs/op
census-income_srt As loaded 1,938.85 432.28 77.70% 3,355,696 1,242
census-income_srt RunOptimize 6,920.47 509.90 92.63% 21,368,432 15,050
census-income As loaded 2,584.34 1,201.73 53.50% 3,729,768 1,476
census-income RunOptimize 2,613.92 1,225.77 53.11% 3,450,377 1,490
census1881_srt As loaded 70.67 18.24 74.19% 84,920 574
census1881_srt RunOptimize 84.67 53.93 36.31% 24,112 835
census1881 As loaded 170.76 28.27 83.44% 303,241 236
census1881 RunOptimize 5,323.17 32.22 99.39% 19,332,214 11,888
dimension_003 As loaded 967.88 334.81 65.41% 493,264 3,084
dimension_003 RunOptimize 524.21 349.15 33.40% 50,616 3,391
dimension_008 As loaded 2,491.85 261.35 89.51% 3,714,613 8,502
dimension_008 RunOptimize 482.86 134.00 72.25% 137,064 9,039
dimension_033 As loaded 2,081.80 400.80 80.75% 3,259,592 1,428
dimension_033 RunOptimize 278.30 108.25 61.10% 175,096 3,088
uscensus2000 As loaded 13.63 12.45 8.72% 696 42
uscensus2000 RunOptimize 13.61 12.47 8.41% 696 42
weather_sept_85_srt As loaded 3,677.43 853.61 76.79% 6,542,955 3,040
weather_sept_85_srt RunOptimize 5,240.80 896.15 82.90% 7,528,918 16,322
weather_sept_85 As loaded 9,522.06 5,523.50 41.99% 13,236,832 5,090
weather_sept_85 RunOptimize 9,795.50 5,636.80 42.46% 13,282,613 5,235
wikileaks-noquotes_srt As loaded 205.96 36.62 82.22% 320,176 1,206
wikileaks-noquotes_srt RunOptimize 2,868.93 28.73 99.00% 9,830,970 10,543
wikileaks-noquotes As loaded 470.02 148.01 68.51% 542,736 1,860
wikileaks-noquotes RunOptimize 2,369.02 186.17 92.14% 4,330,610 15,220

Most datasets have 199 pairs per op; dimension_003 has 15,481, dimension_008 5,239, dimension_033 172. Base 65fb6f8, real-roaring-datasets 929d808.

Container pairs, BenchmarkOrCardinalityContainers and BenchmarkOrCardinalityArray on c8g.large, six 300 ms samples per case, one OrCardinality call over four aligned containers per op:

Container pair Master ns/op This PR ns/op Less time
Bitmap / bitmap 7,498.0 966.2 87.1%
Array / bitmap 47,180.0 12,950.0 72.6%
Run / bitmap 9,065.0 580.9 93.6%
Run / array 14,770.0 9,500.0 35.7%
Run / run 256.9 65.0 74.7%
Array / array, 4,096 each, half overlap 53,210.0 22,100.0 58.5%
Array / array, 4,096 vs 16 54,350.0 871.0 98.4%

The per-container orCardinality methods have existed since 8a91ca4 but
Bitmap.OrCardinality still built the union and read its cardinality, in
both the 32-bit and the 64-bit API.
len(a) + len(b) - |a ∩ b| reuses intersection2by2Cardinality, which
already gallops on skewed sizes, and removes the separate merge loop.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant