Skip to content

Commit c2f941e

Browse files
authored
Clarify Boolean collection code generation (#9676)
## Summary Clarifies the callback contract and code-generation behavior of `BitBuffer::collect_bool`, including the remaining AVX-512 mask-to-bytes-to-mask conversion tracked by [LLVM #219235](llvm/llvm-project#219235). ## Changes Documents when to use the inlined and runtime-multiversioned collectors, and replaces the duplicate `BitBufferMut` explanations with links to the canonical `BitBuffer` documentation. --------- Signed-off-by: Connor Tsui <connor.tsui20@gmail.com>
1 parent eff83d4 commit c2f941e

2 files changed

Lines changed: 33 additions & 48 deletions

File tree

vortex-buffer/src/bit/buf.rs

Lines changed: 29 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -191,39 +191,46 @@ impl BitBuffer {
191191
}
192192
}
193193

194-
/// Invokes `f` with indexes `0..len` collecting the boolean results into a new [`BitBuffer`].
194+
/// Collects `len` Boolean values from `f` into a packed [`BitBuffer`].
195195
///
196-
/// `f` is invoked exactly once per index, in ascending order, and the results are packed
197-
/// with the baseline SIMD byte→bit instruction of the target.
196+
/// Calls `f` exactly once for each index in `0..len`, in order.
197+
///
198+
/// # Code generation
199+
///
200+
/// `collect_bool_words` calls `collect_bool_words_inline`, which selects a `pack_bool_word_*`
201+
/// kernel at compile time and passes it to `collect_bool_words_with`. That shared word loop
202+
/// materializes each full 64-value chunk as a byte-per-value `[bool; 64]`, then passes it to the
203+
/// selected kernel. For simple predicates, LLVM vectorizes the loop and removes the physical
204+
/// stack array. On AVX-512, it still combines the comparison masks, expands the result into 64
205+
/// `0` or `1` bytes with `vpbroadcastq` and `vmovdqu8`, then recreates the mask with `vptestmb`.
206+
/// [LLVM issue #219235](https://github.com/llvm/llvm-project/issues/219235) tracks replacing
207+
/// that round trip with a direct `kmovq` store. The conversion is per chunk. This method does
208+
/// not create a full-column byte buffer.
198209
///
199210
/// # Performance
200211
///
201-
/// The packing is a few instructions per 64 bits, so evaluating `f` is usually the
202-
/// bottleneck. In particular, a bounds-checked slice access in `f` (`|i| values[i] > x`)
203-
/// blocks vectorization of the gather and can cost ~10x the packing itself. Since `f` only
204-
/// ever sees indices `0..len`, callers reading from a slice with `len <= values.len()` may
205-
/// soundly use `|i| unsafe { *values.get_unchecked(i) }`.
212+
/// `collect_bool_words_inline` and `collect_bool_words_with` can inline into the caller, so LLVM
213+
/// sees `f`, the fill loop, and the packing kernel together. A retained bounds check inside `f`
214+
/// can prevent vectorization. A caller that proves `len <= values.len()` can use
215+
/// `unsafe { *values.get_unchecked(i) }` because this method only passes indices in `0..len`.
206216
///
207-
/// Prefer this entry point for every predicate. Only switch to
208-
/// [`Self::collect_bool_multiversioned`] after carefully checking that your specific `f`
209-
/// meets its contract (a trivially cheap, bounds-check-free gather or comparison) —
210-
/// ideally with a benchmark.
217+
/// Use this method for general predicates. Use [`Self::collect_bool_multiversioned`] only for
218+
/// the specialized predicates described there.
211219
#[inline]
212220
pub fn collect_bool<F: FnMut(usize) -> bool>(len: usize, f: F) -> Self {
213221
BitBufferMut::collect_bool(len, f).freeze()
214222
}
215223

216-
/// Like [`Self::collect_bool`], but compiles the packing loop — with `f` inside it — once
217-
/// per CPU feature level (AVX-512BW/AVX2/baseline) and selects a clone by runtime feature
218-
/// detection.
224+
/// Collects Boolean values with a fill-and-pack loop selected for the current CPU.
225+
///
226+
/// This has the same callback contract as [`Self::collect_bool`]. On x86-64,
227+
/// `collect_bool_words_multiversioned` selects `collect_bool_words_avx512`,
228+
/// `collect_bool_words_avx2`, or `collect_bool_words_inline` at runtime. Each wider version is a
229+
/// `#[target_feature]` function, so Rust cannot inline it into a caller compiled without those
230+
/// features.
219231
///
220-
/// Calling this asserts that `f` is small and simple enough (e.g. a bounds-check-free slice
221-
/// gather or comparison) that duplicating it per feature level and paying a
222-
/// `#[target_feature]` call boundary beats inlining it once into your function. For any
223-
/// non-trivial `f` that assertion is false — the boundary deoptimizes the predicate — so
224-
/// unless you have carefully checked (ideally benchmarked) that your specific `f`
225-
/// qualifies, use [`Self::collect_bool`]. See
226-
/// [`collect_bool_words_multiversioned`](crate::bit::collect_bool_words_multiversioned).
232+
/// Use this method only for a small, bounds-check-free predicate whose wider loop has been
233+
/// benchmarked. Use [`Self::collect_bool`] for general predicates.
227234
#[inline]
228235
pub fn collect_bool_multiversioned<F: FnMut(usize) -> bool>(len: usize, f: F) -> Self {
229236
BitBufferMut::collect_bool_multiversioned(len, f).freeze()

vortex-buffer/src/bit/buf_mut.rs

Lines changed: 4 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -189,39 +189,17 @@ impl BitBufferMut {
189189
}
190190
}
191191

192-
/// Invokes `f` with indexes `0..len` collecting the boolean results into a new `BitBufferMut`
192+
/// Mutable-buffer form of [`BitBuffer::collect_bool`].
193193
///
194-
/// `f` is invoked exactly once per index, in ascending order, and the results are packed
195-
/// with the baseline SIMD byte→bit instruction of the target.
196-
///
197-
/// # Performance
198-
///
199-
/// The packing is a few instructions per 64 bits, so evaluating `f` is usually the
200-
/// bottleneck. In particular, a bounds-checked slice access in `f` (`|i| values[i] > x`)
201-
/// blocks vectorization of the gather and can cost ~10x the packing itself. Since `f` only
202-
/// ever sees indices `0..len`, callers reading from a slice with `len <= values.len()` may
203-
/// soundly use `|i| unsafe { *values.get_unchecked(i) }`.
204-
///
205-
/// Prefer this entry point for every predicate. Only switch to
206-
/// [`Self::collect_bool_multiversioned`] after carefully checking that your specific `f`
207-
/// meets its contract (a trivially cheap, bounds-check-free gather or comparison) —
208-
/// ideally with a benchmark.
194+
/// Calls `f` in the same order and uses the same packing path.
209195
#[inline]
210196
pub fn collect_bool<F: FnMut(usize) -> bool>(len: usize, f: F) -> Self {
211197
Self::collect_words(len, |words| collect_bool_words(words, len, f))
212198
}
213199

214-
/// Like [`Self::collect_bool`], but compiles the packing loop — with `f` inside it — once
215-
/// per CPU feature level (AVX-512BW/AVX2/baseline) and selects a clone by runtime feature
216-
/// detection.
200+
/// Mutable-buffer form of [`BitBuffer::collect_bool_multiversioned`].
217201
///
218-
/// Calling this asserts that `f` is small and simple enough (e.g. a bounds-check-free slice
219-
/// gather or comparison) that duplicating it per feature level and paying a
220-
/// `#[target_feature]` call boundary beats inlining it once into your function. For any
221-
/// non-trivial `f` that assertion is false — the boundary deoptimizes the predicate — so
222-
/// unless you have carefully checked (ideally benchmarked) that your specific `f`
223-
/// qualifies, use [`Self::collect_bool`]. See
224-
/// [`collect_bool_words_multiversioned`].
202+
/// Calls `f` in the same order and uses the same packing path.
225203
#[inline]
226204
pub fn collect_bool_multiversioned<F: FnMut(usize) -> bool>(len: usize, f: F) -> Self {
227205
Self::collect_words(len, |words| {

0 commit comments

Comments
 (0)