Vectorize the bitmap-container word operations, fusing the cardinality pass - #559
Merged
Conversation
…y pass
The bitmap-container boolean operations wrote their result with a plain Go
loop over 1024 words and then made a second pass to count it. Add six word
helpers -- orSlice, andSlice, xorSlice, andNotSlice, and the orCardSlice and
andCardSlice variants that also return the population count via VPOPCNTQ --
and use them to replace eight hand-written loops in bitmapcontainer.go.
orBitmap, iorBitmap and iandBitmap now make a single fused pass. andBitmap,
xorBitmap, ixorBitmap, andNotBitmap and iandNotBitmapSurely need the
cardinality before choosing a bitmap or array result, so they keep the
count-first shape and only their write loop is vectorized. lazyIORBitmap and
lazyORBitmap lose their manual four-way unroll.
dst may alias either source: sources are loaded before the destination is
stored within an iteration, which is what the in-place operations rely on.
One 8 KB container on a Xeon Gold 6548N:
write only 599 ns -> 52 ns (11.5x)
write + cardinality 631 ns -> 115 ns (5.5x)
Two bitmaps of 64 dense bitmap containers:
Or 152985 ns -> 90806 ns (1.68x)
AndNot 153366 ns -> 105304 ns (1.46x)
And 152147 ns -> 105404 ns (1.44x)
Xor 151482 ns -> 105158 ns (1.44x)
The end-to-end gap is allocation: each operation allocates 64 fresh 8 KB
containers.
Claude-Session: https://claude.ai/code/session_0123ePBefqPjrCvhxdwWFakj
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
The bitmap-container boolean operations wrote their result with a plain Go loop over 1024 words and then made a second pass to count it. This vectorizes the write and, where the shape allows, fuses the population count into the same pass.
Type of Change
Changes Made
Adds six word-level helpers with the usual three-file layout (
bitsetops.goportable,bitsetops_generic.go,bitsetops_avx512_amd64.go+.s):orSlice,andSlice,xorSlice,andNotSlice-dst[i] = a[i] op b[i]orCardSlice,andCardSlice- the same, also returning the population count of the result viaVPOPCNTQbitmapcontainer.gothen loses eight hand-written word loops:orBitmap,iorBitmapwrote the result and then calledcomputeCardinality(). Both now make one fused pass.iandBitmapcounted first and then wrote unconditionally; also one fused pass now.andBitmap,xorBitmap,ixorBitmap,andNotBitmap,iandNotBitmapSurelyneed the cardinality before deciding whether to produce a bitmap or an array container, so they keep the existing count-first shape and only their write loop is vectorized.lazyIORBitmapandlazyORBitmapwere manually unrolled by four with bounds-check hints; they become a singleorSlicecall.dstmay alias either source: sources are loaded before the destination is stored within an iteration, and iterations never touch each other's words. This matters because the in-place operations pass the same slice as destination and first source.Detection is
cpu.X86.HasAVX512VPOPCNTDQ. The plain writes only need AVX512F, but they are gated together so one flag governs the file. CPUs without it, appengine builds and non-amd64 targets use the portable helpers, which keep exactly the previous two-pass behaviour (popcntSlicethere may itself be AVX2, so a fused scalar loop would not obviously win).Performance Impact
Xeon Gold 6548N (Emerald Rapids), pinned with
taskset, Go 1.26.3.One 8 KB container:
orSlice)End to end on two bitmaps of 64 dense bitmap containers each:
OrAndNotAndXorOrin placeThe gap between 5-11x on the kernel and 1.4-1.7x end to end is allocation: each of these operations allocates 64 fresh 8 KB containers.
On the real-data corpus the effect is smaller still -
FastOr383->348 us oncensus-income(1.10x) and 1799->1728 us onweather_sept_85(1.04x),ParOr1.08x and 1.05x - because those aggregations are dominated by array and run containers and by the priority-queue machinery, not by bitmap-container words. Reporting it so the kernel numbers are not read as end-to-end ones.Testing
go test ./...,roaring64, the smat corpus and smat-hit checks,go tool unconvert,gofmt,go vet, and cross-builds for 386/arm/arm64 and-tags appengine, all on AVX-512 hardware so the new path executes.TestBitsetOpsMatchGocompares all six helpers against the portable versions at lengths 0,1,3,8,31,32,33,64,65,1023,1024,1025 - the empty case, sub-block cases, exact multiples of the 32-word main loop, and the scalar tail.TestBitsetOpsAliasingcoversdst == aanddst == b, which is how the in-place container operations call them.Breaking Changes
None.
Additional Notes
Part of a series applying AVX-512 to roaring. Overlaps with #557 and #558 only in
go.mod/go.sum; worth deciding as a group whether these should share one AVX-512 feature gate instead of one flag per feature.https://claude.ai/code/session_0123ePBefqPjrCvhxdwWFakj