Skip to content

Avoid sorting large BatchEqual queries on small indexes - #588

Open
perfloop-agent wants to merge 5 commits into
RoaringBitmap:masterfrom
perfloop-oss:perfloop-pr-open-hctme26na1
Open

perfloop-agent wants to merge 5 commits into
RoaringBitmap:masterfrom
perfloop-oss:perfloop-pr-open-hctme26na1

Conversation

@perfloop-agent

Copy link
Copy Markdown
Contributor

Description

Avoid sorting large BatchEqual query lists when the index is small. In #532 (dac837d3bbc8, contributed by perfloop-agent), we added sorting of every retained nonempty query before dispatch. That regressed large IN-lists on small indexes, which pay O(m log m) sorting even when there is only one stored column.

Type of Change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Performance improvement
  • Code refactoring
  • Documentation update
  • Test improvements
  • Build/CI changes

Changes Made

What was changed?

  • Select the existing existence-bitmap scan before sorting unordered queries with at least four distinct retained values per stored column and at least 128 retained values.
  • Reuse the deduplication map for scan membership instead of building another map.

Why was it changed?

  • A small index can be scanned without ordering a much larger query list. The four-to-one cutoff is conservative, not a measured crossover. It covers the former 781/782-column boundary shapes below. With fewer than four values per column, the sorted path remains.

How was it changed?

  • Keep filtering and deduplication before selection. Keep the trie, without sorting, for queries already in order, as at base, and for complete contiguous sets, which are put in order directly. Ordered scattered queries keep base performance. Other queries retain sorting, the branch estimate, trie, and scattered-query fallback.
  • Keep the 128-value startup floor: the new cardinality predicate alone does not imply it.
  • Keep results rooted in the existence bitmap and optimize the populated result as before.
  • Add the seven-shape BenchmarkBatchEqualShuffled in BitSliceIndexing/bsi_benchmark_test.go. TestBatchEqualSmallIndex and TestBatchEqualSmallIndexOrdering check results on the new paths. Both also pass at base, so they are not regression tests for the slowdown.

Testing

Go tests passed for the root, BitSliceIndexing, and roaring64 packages; BitSliceIndexing's race-enabled tests also passed. TestBatchEqualSmallIndex and TestBatchEqualSmallIndexOrdering check GetValue equivalence, hits and misses, signed and narrow values, duplicates, unrepresentable values, existence/plane divergence, result isolation, parallelism, run optimization, shuffled contiguous sets with a nonzero minimum, ordered queries, and non-contiguous sets whose first values span the full range.

Formatting

gofmt and go vet passed for the BitSliceIndexing package.

Fuzzing

Not run. The template's FuzzSmat target is not defined in the repository, and smat exercises the root roaring package, not BitSliceIndexing.

Performance Impact

Local run: Go 1.24.4, GOMAXPROCS=4, preloaded 64-plane indexes and deterministically shuffled distinct query values. The one-column queries contain their stored value; the control query matches 100 of 100000 columns. Setup and shuffling are outside the measured complete BatchEqual(0, values) calls.

Pre-#532 is its parent bd4128b. Base is 33586ce with the benchmark; to reproduce it, use this PR's head with the bsi.go change reverted. Fix is this change. All three arms use the same benchmark file. Three measured triples per shape, with randomized arm order, one excluded warmup triple, and one second per invocation.

The table shows separate arm medians and median paired change (fix minus base); percentages use the base median. The 100000-column/100-value rows are a regression control, not a speedup claim.

Columns / values Metric Pre-#532 Base Fix Paired fix − base
1 / 1000 ns/op 27475 89689 36657 -53149 (-59.26%)
1 / 1000 B/op 38242 46217 45913 -304 (-0.66%)
1 / 1000 allocs/op 35 25 28 +3 (+12.00%)
1 / 100000 ns/op 3260264 18773658 3892507 -14881151 (-79.27%)
1 / 100000 B/op 2366005 3169626 3168503 -1017 (-0.03%)
1 / 100000 allocs/op 289 285 282 -3 (-1.05%)
781 / 100000 ns/op 4129440 28808602 4561651 -24246951 (-84.17%)
781 / 100000 B/op 2377681 4079166 3180550 -898557 (-22.03%)
781 / 100000 allocs/op 331 35992 334 -35658 (-99.07%)
782 / 100000 ns/op 4199729 29475724 4659234 -24986306 (-84.77%)
782 / 100000 B/op 2377792 4080133 3180697 -899419 (-22.04%)
782 / 100000 allocs/op 331 36033 334 -35699 (-99.07%)
100 / 12799 ns/op 386886 2526365 488756 -2035884 (-80.59%)
100 / 12799 B/op 298482 520413 405118 -115295 (-22.15%)
100 / 12799 allocs/op 93 4709 95 -4614 (-97.98%)
100000 / 100000 ns/op 22627428 41306756 37907526 -3106072 (-7.52%)
100000 / 100000 B/op 2976350 6145368 3779655 -2364394 (-38.47%)
100000 / 100000 allocs/op 400 663 405 -258 (-38.91%)
100000 / 100 ns/op 17773155 1539790 1506367 -15688 (-1.02%)
100000 / 100 B/op 430845 897471 897520 +51 (+0.01%)
100000 / 100 allocs/op 42 946 947 +1 (+0.11%)
Exact local Go output from the first measured triple

Pre-#532 — first measured triple:

BenchmarkBatchEqualShuffled/Columns1/Values1000-4         	   43291	     27719 ns/op	   38242 B/op	      35 allocs/op
BenchmarkBatchEqualShuffled/Columns1/Values100000-4         	     352	   3451717 ns/op	 2366005 B/op	     289 allocs/op
BenchmarkBatchEqualShuffled/Columns781/Values100000-4         	     278	   4196645 ns/op	 2377715 B/op	     331 allocs/op
BenchmarkBatchEqualShuffled/Columns782/Values100000-4         	     279	   4199729 ns/op	 2377720 B/op	     332 allocs/op
BenchmarkBatchEqualShuffled/Columns100/Values12799-4         	    2877	    407265 ns/op	  298482 B/op	      93 allocs/op
BenchmarkBatchEqualShuffled/Columns100000/Values100000-4         	      46	  22883232 ns/op	 2976350 B/op	     400 allocs/op
BenchmarkBatchEqualShuffled/Columns100000/Values100-4         	      62	  17773155 ns/op	  430695 B/op	      42 allocs/op

Base — first measured triple:

BenchmarkBatchEqualShuffled/Columns1/Values1000-4         	   13606	     88110 ns/op	   46216 B/op	      25 allocs/op
BenchmarkBatchEqualShuffled/Columns1/Values100000-4         	      60	  18519830 ns/op	 3169814 B/op	     285 allocs/op
BenchmarkBatchEqualShuffled/Columns781/Values100000-4         	      39	  28808602 ns/op	 4079058 B/op	   35992 allocs/op
BenchmarkBatchEqualShuffled/Columns782/Values100000-4         	      38	  29325578 ns/op	 4080133 B/op	   36033 allocs/op
BenchmarkBatchEqualShuffled/Columns100/Values12799-4         	     480	   2528393 ns/op	  520411 B/op	    4709 allocs/op
BenchmarkBatchEqualShuffled/Columns100000/Values100000-4         	      27	  42354897 ns/op	 6145368 B/op	     663 allocs/op
BenchmarkBatchEqualShuffled/Columns100000/Values100-4         	     787	   1540521 ns/op	  897471 B/op	     946 allocs/op

Fix — first measured triple:

BenchmarkBatchEqualShuffled/Columns1/Values1000-4         	   32612	     36657 ns/op	   45913 B/op	      28 allocs/op
BenchmarkBatchEqualShuffled/Columns1/Values100000-4         	     322	   3893611 ns/op	 3168461 B/op	     282 allocs/op
BenchmarkBatchEqualShuffled/Columns781/Values100000-4         	     243	   4561651 ns/op	 3180550 B/op	     334 allocs/op
BenchmarkBatchEqualShuffled/Columns782/Values100000-4         	     226	   4907349 ns/op	 3180714 B/op	     334 allocs/op
BenchmarkBatchEqualShuffled/Columns100/Values12799-4         	    2358	    488756 ns/op	  405172 B/op	      95 allocs/op
BenchmarkBatchEqualShuffled/Columns100000/Values100000-4         	      28	  37141852 ns/op	 3780974 B/op	     405 allocs/op
BenchmarkBatchEqualShuffled/Columns100000/Values100-4         	     775	   1524833 ns/op	  897525 B/op	     947 allocs/op

Running Benchmarks

Build each revision's benchmark once, then alternate the revisions for each group. Run each columns/values combination in the table with the corresponding exact selection, for example:

go test -c -o bench.test ./BitSliceIndexing
./bench.test -test.run='^$' -test.bench='^BenchmarkBatchEqualShuffled$/^Columns1$/^Values100000$' -test.count=1 -test.benchtime=1s -test.benchmem
./bench.test -test.run='^$' -test.bench='^BenchmarkBatchEqualShuffled$/^Columns100000$/^Values100$' -test.count=1 -test.benchtime=1s -test.benchmem

For the pre-#532 run, copy this PR's BitSliceIndexing/bsi_benchmark_test.go and the setupRunOptimizedQueryBSI helper from BitSliceIndexing/bsi_test.go into that tree; the helper is needed to compile unrelated benchmarks, not called by these measurements.

Performance Analysis

The small-index scan avoids sorting and the trie's per-branch bitmaps (allocations fall from about 36,000 to 334 at 781 and 782 columns), including both sides of the former 781/782-column boundary and the 12799-value case. Deduplication, the query slice, worker buffers, and bitmap construction remain costs; these results do not establish parity with pre-#532.

The one-column/1000-value case uses more allocations than base. The large-index/small-query control's elapsed estimate is lower in this run, while allocated bytes and allocation count are slightly higher; it is not an improvement claim.

These results cover generated shuffled inputs, not a production query distribution. Indexes with fewer than four query values per column still sort, then use the trie or, at 100,000 or more columns, the scan fallback.

Breaking Changes

None. Public signatures and result semantics are unchanged.

Related Issues

Related to #532 and #533.

Additional Notes

No changes to query filtering or existence-bitmap authority. The comparison uses 33586ce, not the original report's 0cca44e; the BatchEqual implementation is identical at those two upstream revisions.

Case and evidence


Made by Perfloop. Human sponsor: Tomás Senart. To ask for a change, comment or review in plain words. How Perfloop handles pull requests.

Signed-off-by: Perfloop Agent <agent@perfloop.ai>
Signed-off-by: Tomás Senart <ts@perfloop.ai>
Signed-off-by: Perfloop Agent <agent@perfloop.ai>
Signed-off-by: Tomás Senart <ts@perfloop.ai>
Signed-off-by: Perfloop Agent <agent@perfloop.ai>
Signed-off-by: Tomás Senart <ts@perfloop.ai>
Signed-off-by: Perfloop Agent <agent@perfloop.ai>
Signed-off-by: Tomás Senart <ts@perfloop.ai>
Signed-off-by: Perfloop Agent <agent@perfloop.ai>
Signed-off-by: Tomás Senart <ts@perfloop.ai>
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