Repository navigation
Conversation
|
@guymolinari @lemire thanks for the quick turnaround on #570. Could you take a look at #566 and this PR when you have time? This one builds on #566 (its first commit is #566's), so #566 is the place to start. We use roaring in Stellar RPC, the RPC server for the Stellar network, and these two PRs significantly reduce allocations in our event queries, so I'd love to get them upstream rather than maintain a fork. |
|
Thanks @tamirms. These look useful. I checked both changes against the base commit on Linux/amd64, using Go 1.24.4 on an AMD EPYC 9V74. I have two requests before approval: #572: Avoid the large cursor allocation on early-empty intersections I reproduced the dimension_003 allocation increase. The dataset contains 15,482 input bitmaps, so make([]int, len(bitmaps)) requests 123,856 bytes, rounded by Go to a 131,072-byte allocation. Together with the empty result, this explains the reported increase from 80 to 131,152 B/op. The first two inputs already have an empty intersection, but the cursor array is allocated before discovering that. In three isolated runs, median time was approximately 37.5 us/op on the base implementation versus 55.0 us/op with #572. Allocation measurements were 80 B/op versus approximately 131,154 B/op. Could you avoid allocating the full cursor workspace on this early-empty path and add a regression benchmark covering a large input count with an empty first-pair intersection? #566: Benchmark fresh inputs for the in-place cases The current in-place benchmark clones dense once and repeatedly intersects the same result. After the first iteration, it measures an already-filtered bitmap. I reran both cases with a fresh clone before every iteration, excluding cloning from the timed section and checking each result against the expected intersection. Medians of three runs:
The gains hold up with fresh inputs. However, the four-range case measured approximately 258 ns/op with the reused input versus 577 ns/op with fresh inputs. Could you update the in-place benchmarks and reported results to cover fresh inputs? A separate clone-plus-intersection benchmark would also help show the end-to-end cost. The targeted run-container intersection and FastAnd tests passed in my checks. |
8f24124 to
1b53c5d
Compare
…ions
Intersecting a run container with a bitmap built an 8 KiB bitmap
container before knowing whether the result was empty, a small array or
a bitmap, and runContainer16.intersects built a whole container to
answer a boolean.
andBitmapContainer and iandRun16 now count the result first, with the
existing andBitmapContainerCardinality, and build only what is returned;
in place, the gaps between intervals are cleared directly. A run with
more than 64 intervals goes through a stack scratch bitmap instead when
it covers more than 4096 values or the CPU has the VBMI2 vector fill.
intersects returns at the first shared value. An in-place And with a
full range returns the container unchanged, and resetBitmapRange clears
whole words with clear.
Xeon 8375C, BenchmarkRunAndBitmap (added):
And, empty result 2.8 µs -> 594 ns (4.7x)
And, bitmap result 3.7 µs -> 2.0 µs (1.9x)
in-place And, bitmap result 2.2 µs -> 475 ns (4.6x), no allocation
Intersects, disjoint 2.8 µs -> 118 ns (23x), no allocation
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0198cmGsvonq19wTyzKBsw2Z
FastAnd with three or more inputs intersected them pairwise, allocating
an intermediate container per input and key even when the result was
empty.
The input with the fewest keys now drives: each of its keys is looked
up in the first four inputs with stack cursors, and an array key is
also checked for a shared value, before anything is allocated. A key
that survives is intersected count-first: bitmaps in a stack scratch
bitmap, with runs clearing what they do not cover, and arrays through
the existing and and iand. Any further inputs are intersected into the
result one at a time, as before; walking every input for every key was
up to 1.9x slower than pairwise And when many inputs keep their keys.
bitmapContainer.andArray hoists its slice headers out of the loop, a
quarter faster on amd64 and arm64, and ParAnd's worker now calls the
same andKChain.
Xeon 8375C, against pairwise And in the same binary:
jointly-empty 14.0 µs -> 1.9 µs (7.3x), 1 alloc instead of 18
singletons-10000 25.9 µs -> 12.4 µs (2.1x)
array30-runs 486 ns -> 286 ns (1.7x)
BenchmarkRealDataFastAnd (added) is 1.9x to 97x faster than master on
all twelve datasets.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0198cmGsvonq19wTyzKBsw2Z
1b53c5d to
fccdc63
Compare
|
Thanks @guymolinari, both are addressed, and I fixed a few more things while I was there. Both PRs are rebased on current master, and the tables are re-measured on a Xeon 8375C. #572, the cursor allocation. #566, the in-place benchmark. The in-place cases now intersect a fresh copy each iteration, cloned in batches outside the timer, and new Also changed:
|
|
Thanks @tamirms. I reviewed #566 at 1c0052a and #572 at fccdc63 and independently reran the relevant checks with Go 1.24.4 on Linux/amd64, AMD EPYC 9V74. Both original concerns are resolved:
Both full test suites passed, along with the focused appengine-build checks on #572. Additional late-driver, late-empty, copy-on-write, and 1,024-input/256-surviving-key checks passed. Longer runs of the surviving-key benchmark showed no reproducible regression. The scratch masking, container selection, and result ownership paths look sound. Please consider retaining the larger surviving-key workload as a benchmark. My previous objections are resolved; I’m comfortable approving both, with #566 merged first. |
fccdc63 to
52da4fa
Compare
|
Thanks @guymolinari. Done: |
1,024 identical inputs of 256 keys of 32 values each, so every key survives every input: the widest walk, kept as a benchmark as asked in review. Xeon 8375C: 13.3 ms against 13.1 ms for pairwise And, with 522 allocations against 535. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
52da4fa to
64285f3
Compare
Description
FastAndwith three or more inputs intersects them pairwise, allocating an intermediate container for each input and key, even when the final result is empty. This checks each key before allocating anything for it, and counts the result before building it.This depends on #566, so the first commit here is #566's. It uses
clearBitmapGaps,containerFromWordsand the allocation-free runintersectsfrom #566, and GitHub cannot base a pull request on a branch in a fork. Only the second commit,fastaggregation: count before allocating in FastAnd, is new here; the diff shrinks to it once #566 merges.Type of Change
Changes Made
What was changed?
FastAndwalks the keys of the input with the fewest keys and looks each one up in the first four inputs, with a cursor per input on the stack. A key that any of them lacks is skipped without allocating.andandiand, smallest array first. A container holding the whole key is skipped.Andpath.bitmapContainer.andArrayis a quarter faster on amd64 and arm64 with its slice headers hoisted out of the loop, whichAnd(bitmap, array)gets too.ParAnd's worker ran the same loop as the newandKChainand now calls it.BenchmarkFastAndShapesandBenchmarkRealDataFastAnd.Why was it changed?
How was it changed?
And.Testing
fastand_kway_test.gocomparesFastAndwith pairwiseAnd, which intersects the first two inputs and then each next input into the result, on 400 random cases of 3 to 12 inputs mixing array, bitmap and run containers, including whole-key ranges, over keys the inputs share but each sometimes lacks. About a third have a non-empty result, and the test requires more than a quarter.Validate, use arrays and bitmaps by the usual size rule, and share no storage with the inputs.All pass on linux/amd64 (Xeon 8375C, with AVX-512 VBMI2), darwin/arm64, and amd64 without AVX-512.
Formatting
gofmt -s -l .andgo tool unconvert ./...report nothing.go vet ./...reports only the existingReadFromsignature warning from master.Fuzzing
The
FuzzSmattarget in the template does not exist on master (smat.gorefers to a missingsmat_fuzz_test.go), so-fuzz=FuzzSmatmatches nothing. The smat corpus runs and passes, but none of its sequences callsFastAnd:The randomized comparison under Testing is what exercises the change.
Performance Impact
BenchmarkFastAndShapes(added) comparesFastAndwith pairwiseAnd, which is whatFastAnddid before, on the same inputs and in the same binary, so both use the same kernels. One key unless noted; the first seven rows add a whole-key range, like a query window, as the last input. Xeon 8375C:AndFastAndarray30-bitmaparray4000-bitmaparray30-runsarray4000-runsbitmap-bitmapruns-bitmapruns-runsjointly-emptyeight-keyssingletons-10000equal-x128surviving-1024x256BenchmarkRealDataFastAnd(added), master against this PR. Each dataset's full intersection is empty, so this measures how quicklyFastAndfinds that out:census-incomecensus-income_srtcensus1881census1881_srtdimension_003dimension_008dimension_033uscensus2000weather_sept_85weather_sept_85_srtwikileaks-noquoteswikileaks-noquotes_srtPerformance Analysis
And, and every real dataset is 1.9x to 97x faster than master. All but two datasets allocate only the result;dimension_008andweather_sept_85have keys that only a later input rules out, and allocate no more than before.FastAnddoes the same intersections plus the walk and the checks, so those rows stay within a few percent of pairwiseAnd.jointly-emptygain is smaller, 13.9 µs to 5.0 µs, becauseandCardSlicehas no NEON version.BenchmarkIntersection*,BenchmarkUnion*andBenchmarkAndAny/*are unchanged.Breaking Changes
None. Results are the same as with pairwise
And.Related Issues
Related to #566.
Additional Notes
arrayContainer.iandBitmaphas the loopbitmapContainer.andArrayhad, and the same change would speed it up; that is left for a separate change.🤖 Generated with Claude Code