From da08a30a7a32133c17377a0be6433a7b8b9f12ff Mon Sep 17 00:00:00 2001 From: Connor Tsui Date: Thu, 29 Oct 2026 10:13:30 -0400 Subject: [PATCH] Avoid a per-run zip_eq call in run-end bool decoding The prefill loops in the run-end bool decoder iterated with `itertools::zip_eq`. `ZipEq::next` is generic and has no `#[inline]`, so rustc emits it in a single codegen unit. When the decoder lands in a different unit, the loop calls it out of line once per run. A change elsewhere in the crate moved the decoder this way in the bench profile (16 codegen units, no LTO), and `decode_bool_nullable` slowed down by 17 to 25% in the CodSpeed simulation. The loops now use `zip`, like the sequential paths in the same file. `runend_decode_bools` checks once that the run ends and the values have the same length, which keeps the check that `zip_eq` made on each run. Signed-off-by: Connor Tsui --- .../src/arrays/runend/decompress_bool.rs | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/vortex-array/src/arrays/runend/decompress_bool.rs b/vortex-array/src/arrays/runend/decompress_bool.rs index ebfea15d6fb..739fb968276 100644 --- a/vortex-array/src/arrays/runend/decompress_bool.rs +++ b/vortex-array/src/arrays/runend/decompress_bool.rs @@ -6,7 +6,6 @@ //! Uses an adaptive strategy that pre-fills the buffer with the majority value //! (0s or 1s) and only fills the minority runs, minimizing work for skewed distributions. -use itertools::Itertools; use vortex_buffer::BitBuffer; use vortex_buffer::BitBufferMut; use vortex_error::VortexResult; @@ -31,6 +30,10 @@ use crate::validity::Validity; const PREFILL_RUN_THRESHOLD: usize = 32; /// Decodes run-end encoded boolean values into a flat `BoolArray`. +/// +/// # Panics +/// +/// Panics if `ends` and `values` have different lengths. pub fn runend_decode_bools( ends: PrimitiveArray, values: BoolArray, @@ -38,6 +41,16 @@ pub fn runend_decode_bools( length: usize, ctx: &mut ExecutionCtx, ) -> VortexResult { + // NB: The decode loops pair run ends with values through `zip`, which stops at the shorter + // input, so this check must stay. Do not switch the loops to `itertools::zip_eq`: `ZipEq::next` + // has no `#[inline]`, so rustc can place it in another codegen unit, and the loop then calls it + // out of line once per run. + assert_eq!( + ends.len(), + values.len(), + "run ends and values must have the same length" + ); + let validity = values .as_ref() .validity()? @@ -171,7 +184,7 @@ fn decode_bool_non_nullable( let mut decoded = BitBufferMut::full(prefill, length); let mut current_pos = 0usize; - for (end, value) in run_ends.zip_eq(values.iter()) { + for (end, value) in run_ends.zip(values.iter()) { if end > current_pos && value != prefill { // SAFETY: current_pos < end <= length == decoded.len() unsafe { decoded.fill_range_unchecked(current_pos, end, value) }; @@ -203,7 +216,7 @@ fn decode_bool_nullable( let mut decoded_validity = BitBufferMut::full(prefill_valid, length); let mut current_pos = 0usize; - for (end, (value, is_valid)) in run_ends.zip_eq(values.iter().zip(validity_mask.iter())) { + for (end, (value, is_valid)) in run_ends.zip(values.iter().zip(validity_mask.iter())) { if end > current_pos { // SAFETY: current_pos < end <= length == decoded.len() == decoded_validity.len() if is_valid != prefill_valid {