feat[buffer]: specialized collect_bool per arch#8749
Open
joseph-isaacs wants to merge 1 commit into
Open
Conversation
Merging this PR will improve performance by 17.23%
Performance Changes
Tip Curious why this is faster? Comment Comparing Footnotes
|
465885d to
885e3ca
Compare
Replace the bit-at-a-time shift-OR loop behind collect_bool_word / collect_bool_words with byte-to-bit packing kernels specialized per architecture, without changing any public signature: - x86-64 AVX-512BW: one vptestmb packs 64 bools into a u64 - x86-64 AVX2: two vpcmpeqb + vpmovmskb halves - x86-64 SSE2 (baseline): four pcmpeqb + pmovmskb quarters - aarch64 NEON (baseline): ushl by bit position + addp reduction tree - other targets and Miri: branch-free SWAR multiply fallback The API has two tiers. The default (collect_bool / collect_bool_words, backed by collect_bool_words_inline) compiles the word loop once with the widest kernel enabled at compile time (SSE2/NEON on stock targets; AVX2/AVX-512 under -C target-cpu=native) so the predicate, the [bool; 64] materialization, and the pack all inline into the caller. The opt-in tier (collect_bool_multiversioned / collect_bool_words_multiversioned) compiles the loop with the predicate inside once per CPU feature level and selects a clone by runtime feature detection; its docs state the caller's assertion — the predicate must be trivially cheap, because the #[target_feature] call boundary deoptimizes non-trivial predicates (an early all-multiversioned draft regressed FSST fsst_prefix 13-63% this way; the pack kernel choice itself moves that DFA-bound workload by at most ~4%). Route the call sites whose predicates are bounds-check-free gathers or comparisons through the multiversioned tier: From<&[bool]> / From<&[u8]>, ByteBool boolean decoding, and the primitive and decimal between kernels (now reading via get_unchecked, sound because the predicate only receives indices 0..len — a contract now documented on every entry point together with the bounds-check performance note). Measured on AVX-512 hardware (divan, 64Ki bits, medians): the fused word loop with a bool-gather predicate runs 14-24x faster than the old scalar loop per SIMD level (SSE2 1.7us, AVX2 1.2us, AVX-512 1.1us vs 26.5us); BitBufferMut::from(&[bool]) improves ~43x; a between-shaped predicate doubles under the multiversioned tier (5.5us vs 11.2us); fsst_prefix is unchanged or slightly better than before the change. Generated asm verified per kernel (the fused AVX-512 loop is vmovdqu64 + vptestmb + kmovq per 64 bools; NEON is 14 instructions, its addp tree additionally validated against a reference model on 10,004 random inputs). New benches cover each kernel and both tiers; under CodSpeed only the shipped entry points are tracked (from_bool_slice, collect_bool_u32_gt). Signed-off-by: Claude <noreply@anthropic.com> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TcsiAHXQc4e11yYcP3a3NX
885e3ca to
e64d06d
Compare
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.
Rationale for this change
Currently LLVM cannot create a
bitcast <64 x i1> -> i64instruction (https://llvm.org/docs/LangRef.html#bitcast-to-instruction), so the auto-vectorizer never turnscollect_bool's bit-at-a-time shift-OR loop into byte→bit packing instructions. This means we must add arch-specificcollect_boolimpls, as done here: modern ISAs pack 64 bytes → 64 bits in 1–4 instructions (vptestmb,vpmovmskb,pmovmskb, NEONushl+addp), while the scalar loop pays ~1 cycle per element on everycollect_bool-based kernel (compare, between, filter/take, LIKE, boolean decode,From<&[bool]>).The catch: runtime-detected wide kernels must live behind a
#[target_feature]function boundary that cannot inline into callers, and an early draft that put every predicate behind that boundary regressed FSST's DFA-scanfsst_prefixbenchmarks by 13–63%. The design therefore has two tiers, with the boundary trade-off documented on every entry point.What changes are included in this PR?
vortex-buffer/src/bit/pack.rswith byte→bit pack kernels per architecture: AVX-512BW (vptestmb), AVX2 (vpcmpeqb+vpmovmskb), SSE2 (pcmpeqb+pmovmskb), aarch64 NEON (ushl+addptree), and a portable SWAR multiply fallback (also used under Miri). Each kernel is tested against the scalar reference; the NEON reduction tree was additionally validated against a reference model on 10,004 random inputs.collect_bool/collect_bool_words, unchanged signatures): the word loop compiles once with the widest compile-time-enabled kernel (SSE2/NEON on stock targets, AVX2/AVX-512 under-C target-cpu=native), so the predicate, the[bool; 64]materialization, and the pack all inline into the caller. Safe for arbitrary predicates —fsst_prefixis unchanged-to-slightly-better vsdevelop.collect_bool_multiversioned/collect_bool_words_multiversioned): compiles the loop with the predicate inside once per CPU feature level and selects a clone by runtime detection. Docs state the caller's assertion: the predicate must be trivially cheap, because the boundary deoptimizes non-trivial ones. Lengths under one word skip detection entirely.From<&[bool]>/From<&[u8]>, ByteBool boolean decode, and the primitive/decimalbetweenkernels (now reading viaget_unchecked, sound because the predicate only receives indices0..len— a contract now documented on the entry points).benches/collect_bool.rscovering each kernel and both tiers; only the shipped entry points (from_bool_slice,collect_bool_u32_gt) are tracked under CodSpeed — baselines and per-variant loops compile out.Measured on AVX-512 hardware (divan, 64Ki bits, medians): the fused word loop with a bool-gather predicate runs 14–24× faster than the scalar loop (SSE2 1.7µs / AVX2 1.2µs / AVX-512 1.1µs vs 26.5µs);
BitBufferMut::from(&[bool])improves ~43×; a between-shaped predicate doubles under the multiversioned tier; generated asm verified per kernel (the fused AVX-512 loop isvmovdqu64+vptestmb+kmovqper 64 bools).What APIs are changed? Are there any user-facing changes?
No existing signatures change. New public API in
vortex-buffer:BitBuffer::collect_bool_multiversioned,BitBufferMut::collect_bool_multiversioned,collect_bool_words_multiversioned,collect_bool_word_scalar, the per-arch pack kernels (pack_bool_word_{swar,sse2,avx2,avx512,neon}), and the per-arch word loops (collect_bool_words_{sse2,avx2,avx512,neon}, kept public so each variant stays individually testable and benchable). All documented, with the multiversioned entries steering readers back tocollect_boolunless their specific predicate has been checked (ideally benchmarked).🤖 Generated with Claude Code
https://claude.ai/code/session_01TcsiAHXQc4e11yYcP3a3NX