Conversation
…ions
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>
FastAnd with three or more inputs intersected pairwise and materialized
an intermediate per input per key, so a key whose result was empty still
allocated up to 8 KiB per step, and a run container among bitmaps fell
back to the same pairwise path.
With three or more inputs the result is now counted before it is
allocated. The input with the fewest containers drives; its keys are
walked once, in order, against every other input with advanceUntil, and
a key every input has is probed with a container intersects test before
its containers are intersected, so a key with nothing in common costs
no allocation and the call allocates nothing but the answer. Two inputs
keep the existing And path.
andK intersects one key's containers. Bitmaps are ANDed into stack
scratch with the fused andCardSlice, stopping at the first empty prefix,
over the words the runs leave: a run narrows the span to its extent, and
only a run with gaps is folded into a mask afterwards, so a range built
with AddRange costs no intersection and a result the bitmaps already
rule out never walks an interval. The result is copied out as a bitmap,
appended as an exact-size array, or dropped. A key with an array, or of
runs only, goes through the library's own kernels instead: a full run
is the identity and drops out, the smallest array leads and the rest
keep the caller's order, the first pair into a fresh container and the
rest in place, so nothing bigger than that first result is built. The
first step for an array under a bitmap is bitmapContainer.andArray,
whose loop reloaded three slice headers per element; with them hoisted
it is a quarter faster on both amd64 and arm64.
BenchmarkFastAndShapes (added) runs each kernel path against the
pairwise chain on the same inputs, one key unless noted, Xeon 8375C:
pairwise chain FastAnd
array30-bitmap 231 ns 200 B 6 allocs 269 ns 200 B 6 allocs
array4000-bitmap 6.6 µs 8328 B 6 allocs 6.1 µs 8328 B 6 allocs
array30-runs 938 ns 288 B 8 allocs 928 ns 200 B 6 allocs
array4000-runs 5.7 µs 16544 B 8 allocs 3.7 µs 8328 B 6 allocs
bitmap-bitmap 2.5 µs 8336 B 6 allocs 2.6 µs 8336 B 6 allocs
runs-bitmap 5.8 µs 6304 B 8 allocs 5.0 µs 4232 B 6 allocs
runs-runs 8.3 µs 10400 B 8 allocs 7.5 µs 8328 B 6 allocs
jointly-empty 12.5 µs 33200 B 18 allocs 1.9 µs 80 B 1 allocs
eight-keys 1.4 µs 1056 B 24 allocs 1.5 µs 1056 B 24 allocs
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
FastAndwith three or more inputs intersects pairwise and materializes an intermediate per input per key. This counts each key's result before allocating it.This depends on #566, and that is why the first of the two commits here is #566's. This change needs
clearBitmapGaps,containerFromWordsand the allocation-free runintersectsthat #566 introduces, and GitHub cannot base a pull request on a branch that lives in a fork. Only the second commit,fastaggregation: count before allocating in FastAnd, is under review here. The diff narrows to that commit alone as soon as #566 merges.Type of Change
Changes Made
What was changed?
FastAnddrives from the input with the fewest containers and walks its keys once, in order, against every other input withadvanceUntil. A key every input has is probed with a containerintersectstest before its containers are intersected. Two inputs keep the existingAndpath.andKintersects one key's containers. Bitmaps are ANDed into stack scratch with the fusedandCardSlice, stopping at the first empty prefix, over the words the runs leave. The result is copied out as a bitmap, appended as an exact-size array, or dropped.bitmapContainer.andArrayreloaded three slice headers per element; hoisting them makes it a quarter faster on both amd64 and arm64, whichAnd(bitmap, array)gets as well.BenchmarkFastAndShapesandBenchmarkRealDataFastAnd, the latter next toBenchmarkRealDataFastOr.Why was it changed?
How was it changed?
AddRangecosts no intersection and a result the bitmaps already rule out never walks an interval.Testing
fastand_kway_test.gocomparesFastAndwith pairwiseAndover 400 random cases of 3 to 6 inputs mixing array, bitmap and run containers with keys that interleave and go missing. It checks that results share no storage with the inputs, covers the run-mask edges (whole key, short and word-straddling intervals, a thousand short intervals, runs folding to a narrower mask, one bitmap under two runs, two ranges that overlap only where the bitmap is empty, runs only, disjoint key sets), and asserts that an empty intersection allocates nothing but the answer.Results are checked with
ValidateandEquals, and the array and bitmap sizing rule holds for every container.All pass on linux/amd64 and darwin/arm64.
Formatting
gofmt -s -l .reports nothing.go vet ./...reports only the pre-existingReadFromsignature complaint on master.Fuzzing
The step above cannot be run as written: there is no
FuzzSmattarget on master.smat.gorefers to asmat_fuzz_test.gothat is not in the repository, sogo test -fuzz=FuzzSmatmatches nothing and simply runs the ordinary tests. What does run is the smat corpus, which passes:Performance Impact
Xeon 8375C.
BenchmarkFastAndShapesis added: each kernel path against the pairwise chain on the same inputs, one key unless noted.BenchmarkRealDataFastAndis added, before and after:The intersection of a whole dataset is empty, so that table measures the key narrowing and the early exit on real container shapes.
Performance Analysis
FastAndis faster by 18% at the median and by more than 20% in 145 cells.andCardSlicehas no NEON version and runs as the Go loop there.BenchmarkIntersection*,BenchmarkUnion*andBenchmarkAndAny/*are unchanged.Breaking Changes
None. The result is the same as the pairwise
Andchain.Related Issues
Related to #566.
Additional Notes
The
bitmapContainer.andArrayhoist is not specific toFastAnd:And(bitmap, array)andIntersectsget the same improvement.🤖 Generated with Claude Code