Speed up the AVX-512 VBMI2 bitset decoders - #868
Merged
Conversation
Both bitset_extract_setbits_avx512 and bitset_extract_setbits_avx512_uint16
kept the VPCOMPRESSB kernel but stored full 64-byte blocks unconditionally,
relying on the next word to overwrite the slack. That cost two things: the
caller had to leave 64 values of padding (so the last values always fell back
to the scalar tail), and every word paid for all four blocks even when it only
produced a handful of values.
Each block is now stored under a mask derived from BZHI(-1, popcount), so a
store writes exactly the values it produced, and only the ceil(popcount/16)
blocks that carry a value are computed at all. Empty words are skipped and the
base is carried in a ZMM incremented by a constant instead of being rebuilt
per word.
Measured on a Xeon Gold 6548N (Emerald Rapids), ns per value, kernel only:
cardinality scalar before after
256 2.91 6.93 2.95
1024 1.29 1.95 1.02
2048 0.76 1.07 0.65
4096 0.54 0.54 0.37
8192 0.42 0.27 0.18
The uint32 decoder now beats the scalar loop at every cardinality measured
from 16 to 65536, so bitset_container_to_uint32_array no longer needs its
cardinality heuristic.
array_container_from_bitset is the opposite case: every caller checks that the
result fits an array container first, so its input is always under 6.25%
density, and it had no heuristic at all. The old kernel was a net regression
there -- 2.4x slower than the scalar loop at 256 values. The new one is faster
from about 1024 values up, so that is where the gate goes.
End to end, with the repository's own benchmark:
microbench/ToArray/census-income 6671 ns -> 4705 ns (1.42x)
microbench/ToArray/weather_sept_85 25895 ns -> 11611 ns (2.23x)
bitset_container/to_array_convert 2.08 ns -> 1.47 ns (1.42x)
The other datasets hold no bitset containers and are unchanged. Benchmarks
outside these paths move by up to 1.5x in either direction, but rebuilding the
unmodified source reproduces the same swings, so they are code layout noise.
The masked-store idea is from RoaringBitmap/roaring#557, which applies it to
the Go port's ToArray.
A new test compares both decoders against the scalar ones across 64 densities,
at exactly the cardinality and at truncated capacities.
Claude-Session: https://claude.ai/code/session_015iZSVG9KJ595ktzGqaK7qR
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.
What
bitset_extract_setbits_avx512andbitset_extract_setbits_avx512_uint16keep theVPCOMPRESSBkernel, but change how it stores.Before, each word stored full 64-byte blocks unconditionally and let the next word overwrite the slack. That cost two things: the caller had to leave 64 values of padding (
(out + 64) < safeout), so the tail of every container fell back to the scalar loop; and every word paid for all four blocks even when it produced a handful of values.Now each block is stored under a mask derived from
BZHI(-1, popcount), so a store writes exactly the values it produced, and only theceil(popcount/16)blocks that carry a value are computed at all. Empty words are skipped, and the base is carried in a ZMM incremented by a constant rather than rebuilt per word.The masked-store idea comes from RoaringBitmap/roaring#557, which applies it to the Go port's
ToArray.Kernel measurements
Xeon Gold 6548N (Emerald Rapids),
taskset-pinned, ns per value, kernel only:The two call sites
bitset_container_to_uint32_array— the AVX-512 kernel now beats the scalar loop at every cardinality measured from 16 to 65536, in both streaming and cache-resident modes, so itscardinality >= 8192heuristic is gone. The AVX2 gate is untouched.array_container_from_bitset— the opposite case. Every caller checks that the result fits an array container first, so the input is always under 6.25% density, and this call site had no heuristic at all. The old kernel was a net regression here: at 256 values it was 2.4x slower than the scalar loop (6.93 vs 2.91 ns/value). The new kernel is faster from about 1024 values up, so that is where the gate goes.End to end
Using this repository's own benchmark:
microbench/ToArray/census-incomemicrobench/ToArray/weather_sept_85bitset_container/to_array_convertThe other datasets contain no bitset containers and are unchanged.
Benchmarks outside these paths move by up to 1.5x in either direction, but that is code-layout noise, not this change: rebuilding the unmodified source into a second binary reproduces the same swings (
synthetic/ContainsColdHighis 1.496x on the rebuild control and 1.490x here;microbench/ComputeCardinalityis 1.163x and 1.165x). The two benchmarks that do touch this code are 1.005x and 1.003x across rebuilds and 0.705x with the patch.Feature detection
Unchanged, and already correct:
CROARING_AVX512_REQUIREDincludesCROARING_AVX512VBMI2(CPUID leaf 7, ECX bit 6) with thexgetbvopmask/ZMM check, the compile-time fast path requires__AVX512VBMI2__,CROARING_TARGET_AVX512targetsavx512vbmi2, andCROARING_COMPILER_SUPPORTS_AVX512keys off<avx512vbmi2intrin.h>.ROARING_SUPPORTS_AVX512already implies VBMI2 is usable.Testing
avx512_extract_matches_scalarintests/util_unit.ccompares both decoders against the scalar ones across 64 densities, at exactly the cardinality and at truncated capacities.Note for a follow-up
array_array_container_union(mixed_union.c) does the same bitset-to-array conversion on the scalar path with no dispatch. That branch is reachable only with cardinality in [2049, 4096], which is entirely inside the range where the vector kernel wins, so it looks like a free 1.2-1.4x on that step. Left out of this PR to keep it to the two decoders.https://claude.ai/code/session_015iZSVG9KJ595ktzGqaK7qR