Skip to main content

vortex_runend/
decompress_bool.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright the Vortex contributors
3
4//! Optimized run-end decoding for boolean arrays.
5//!
6//! Uses an adaptive strategy that pre-fills the buffer with the majority value
7//! (0s or 1s) and only fills the minority runs, minimizing work for skewed distributions.
8
9use itertools::Itertools;
10use vortex_array::ArrayRef;
11use vortex_array::ExecutionCtx;
12use vortex_array::IntoArray;
13use vortex_array::arrays::BoolArray;
14use vortex_array::arrays::ConstantArray;
15use vortex_array::arrays::PrimitiveArray;
16use vortex_array::arrays::bool::BoolArrayExt;
17use vortex_array::dtype::DType;
18use vortex_array::dtype::Nullability;
19use vortex_array::match_each_unsigned_integer_ptype;
20use vortex_array::scalar::Scalar;
21use vortex_array::validity::Validity;
22use vortex_buffer::BitBuffer;
23use vortex_buffer::BitBufferMut;
24use vortex_error::VortexResult;
25use vortex_mask::Mask;
26
27use crate::iter::trimmed_ends_iter;
28
29/// Threshold for number of runs below which we use sequential append instead of prefill.
30/// With few runs, the overhead of prefilling the entire buffer dominates.
31const PREFILL_RUN_THRESHOLD: usize = 32;
32
33/// Decodes run-end encoded boolean values into a flat `BoolArray`.
34pub fn runend_decode_bools(
35    ends: PrimitiveArray,
36    values: BoolArray,
37    offset: usize,
38    length: usize,
39    ctx: &mut ExecutionCtx,
40) -> VortexResult<ArrayRef> {
41    let validity = values
42        .as_ref()
43        .validity()?
44        .execute_mask(values.as_ref().len(), ctx)?;
45    let values_buf = values.to_bit_buffer();
46    let nullability = values.dtype().nullability();
47
48    // Fast path for few runs with no offset - avoids iterator overhead
49    let num_runs = values_buf.len();
50    if offset == 0 && num_runs < PREFILL_RUN_THRESHOLD {
51        return Ok(match_each_unsigned_integer_ptype!(ends.ptype(), |E| {
52            decode_few_runs_no_offset(
53                ends.as_slice::<E>(),
54                &values_buf,
55                validity,
56                nullability,
57                length,
58            )
59        }));
60    }
61
62    Ok(match_each_unsigned_integer_ptype!(ends.ptype(), |E| {
63        runend_decode_typed_bool(
64            trimmed_ends_iter(ends.as_slice::<E>(), offset, length),
65            &values_buf,
66            validity,
67            nullability,
68            length,
69        )
70    }))
71}
72
73/// Decodes run-end encoded boolean values using an adaptive strategy.
74///
75/// The strategy counts true vs false runs and chooses the optimal approach:
76/// - If more true runs: pre-fill with 1s, clear false runs
77/// - If more false runs: pre-fill with 0s, fill true runs
78///
79/// This minimizes work for skewed distributions (e.g., sparse validity masks).
80pub fn runend_decode_typed_bool(
81    run_ends: impl Iterator<Item = usize>,
82    values: &BitBuffer,
83    values_validity: Mask,
84    values_nullability: Nullability,
85    length: usize,
86) -> ArrayRef {
87    match values_validity {
88        Mask::AllTrue(_) => {
89            decode_bool_non_nullable(run_ends, values, values_nullability, length).into_array()
90        }
91        Mask::AllFalse(_) => {
92            ConstantArray::new(Scalar::null(DType::Bool(Nullability::Nullable)), length)
93                .into_array()
94        }
95        Mask::Values(mask) => {
96            decode_bool_nullable(run_ends, values, mask.bit_buffer(), length).into_array()
97        }
98    }
99}
100
101/// Fast path for few runs with no offset. Uses direct slice access to minimize overhead.
102/// This avoids the `trimmed_ends_iter` iterator chain which adds significant overhead
103/// for small numbers of runs.
104#[inline(always)]
105fn decode_few_runs_no_offset<E: vortex_array::dtype::IntegerPType>(
106    ends: &[E],
107    values: &BitBuffer,
108    validity: Mask,
109    nullability: Nullability,
110    length: usize,
111) -> ArrayRef {
112    match validity {
113        Mask::AllTrue(_) => {
114            let mut decoded = BitBufferMut::with_capacity(length);
115            let mut prev_end = 0usize;
116            for (i, &end) in ends.iter().enumerate() {
117                let end = end.as_().min(length);
118                decoded.append_n(values.value(i), end - prev_end);
119                prev_end = end;
120            }
121            BoolArray::new(decoded.freeze(), nullability.into()).into_array()
122        }
123        Mask::AllFalse(_) => {
124            ConstantArray::new(Scalar::null(DType::Bool(Nullability::Nullable)), length)
125                .into_array()
126        }
127        Mask::Values(mask) => {
128            let validity_buf = mask.bit_buffer();
129            let mut decoded = BitBufferMut::with_capacity(length);
130            let mut decoded_validity = BitBufferMut::with_capacity(length);
131            let mut prev_end = 0usize;
132            for (i, &end) in ends.iter().enumerate() {
133                let end = end.as_().min(length);
134                let run_len = end - prev_end;
135                let is_valid = validity_buf.value(i);
136                if is_valid {
137                    decoded_validity.append_n(true, run_len);
138                    decoded.append_n(values.value(i), run_len);
139                } else {
140                    decoded_validity.append_n(false, run_len);
141                    decoded.append_n(false, run_len);
142                }
143                prev_end = end;
144            }
145            BoolArray::new(decoded.freeze(), Validity::from(decoded_validity.freeze())).into_array()
146        }
147    }
148}
149
150/// Decodes run-end encoded booleans when all values are valid (non-nullable).
151fn decode_bool_non_nullable(
152    run_ends: impl Iterator<Item = usize>,
153    values: &BitBuffer,
154    nullability: Nullability,
155    length: usize,
156) -> BoolArray {
157    let num_runs = values.len();
158
159    // For few runs, sequential append is faster than prefill + modify
160    if num_runs < PREFILL_RUN_THRESHOLD {
161        let mut decoded = BitBufferMut::with_capacity(length);
162        for (end, value) in run_ends.zip(values.iter()) {
163            decoded.append_n(value, end - decoded.len());
164        }
165        return BoolArray::new(decoded.freeze(), nullability.into());
166    }
167
168    // Adaptive strategy: prefill with majority value, only flip minority runs
169    let prefill = values.true_count() > num_runs - values.true_count();
170    let mut decoded = BitBufferMut::full(prefill, length);
171    let mut current_pos = 0usize;
172
173    for (end, value) in run_ends.zip_eq(values.iter()) {
174        if end > current_pos && value != prefill {
175            // SAFETY: current_pos < end <= length == decoded.len()
176            unsafe { decoded.fill_range_unchecked(current_pos, end, value) };
177        }
178        current_pos = end;
179    }
180    BoolArray::new(decoded.freeze(), nullability.into())
181}
182
183/// Decodes run-end encoded booleans when values may be null (nullable).
184fn decode_bool_nullable(
185    run_ends: impl Iterator<Item = usize>,
186    values: &BitBuffer,
187    validity_mask: &BitBuffer,
188    length: usize,
189) -> BoolArray {
190    let num_runs = values.len();
191
192    // For few runs, sequential append is faster than prefill + modify
193    if num_runs < PREFILL_RUN_THRESHOLD {
194        return decode_nullable_sequential(run_ends, values, validity_mask, length);
195    }
196
197    // Adaptive strategy: prefill each buffer with its majority value
198    let prefill_decoded = values.true_count() > num_runs - values.true_count();
199    let prefill_valid = validity_mask.true_count() > num_runs - validity_mask.true_count();
200
201    let mut decoded = BitBufferMut::full(prefill_decoded, length);
202    let mut decoded_validity = BitBufferMut::full(prefill_valid, length);
203    let mut current_pos = 0usize;
204
205    for (end, (value, is_valid)) in run_ends.zip_eq(values.iter().zip(validity_mask.iter())) {
206        if end > current_pos {
207            // SAFETY: current_pos < end <= length == decoded.len() == decoded_validity.len()
208            if is_valid != prefill_valid {
209                unsafe { decoded_validity.fill_range_unchecked(current_pos, end, is_valid) };
210            }
211            // Decoded bit should be the actual value when valid, false when null.
212            let want_decoded = is_valid && value;
213            if want_decoded != prefill_decoded {
214                unsafe { decoded.fill_range_unchecked(current_pos, end, want_decoded) };
215            }
216            current_pos = end;
217        }
218    }
219    BoolArray::new(decoded.freeze(), Validity::from(decoded_validity.freeze()))
220}
221
222/// Sequential decode for few runs - avoids prefill overhead.
223#[inline(always)]
224fn decode_nullable_sequential(
225    run_ends: impl Iterator<Item = usize>,
226    values: &BitBuffer,
227    validity_mask: &BitBuffer,
228    length: usize,
229) -> BoolArray {
230    let mut decoded = BitBufferMut::with_capacity(length);
231    let mut decoded_validity = BitBufferMut::with_capacity(length);
232
233    for (end, (value, is_valid)) in run_ends.zip(values.iter().zip(validity_mask.iter())) {
234        let run_len = end - decoded.len();
235        if is_valid {
236            decoded_validity.append_n(true, run_len);
237            decoded.append_n(value, run_len);
238        } else {
239            decoded_validity.append_n(false, run_len);
240            decoded.append_n(false, run_len);
241        }
242    }
243
244    BoolArray::new(decoded.freeze(), Validity::from(decoded_validity.freeze()))
245}
246
247#[cfg(test)]
248mod tests {
249    use std::sync::LazyLock;
250
251    use vortex_array::VortexSessionExecute;
252    use vortex_array::arrays::BoolArray;
253    use vortex_array::arrays::PrimitiveArray;
254    use vortex_array::arrays::bool::BoolArrayExt;
255    use vortex_array::assert_arrays_eq;
256    use vortex_array::validity::Validity;
257    use vortex_buffer::BitBuffer;
258    use vortex_error::VortexResult;
259    use vortex_session::VortexSession;
260
261    use super::runend_decode_bools;
262
263    static SESSION: LazyLock<VortexSession> = LazyLock::new(|| {
264        let session = vortex_array::array_session();
265        crate::initialize(&session);
266        session
267    });
268
269    #[test]
270    fn decode_bools_alternating() -> VortexResult<()> {
271        let mut ctx = SESSION.create_execution_ctx();
272        // Alternating true/false: [T, T, F, F, F, T, T, T, T, T]
273        let ends = PrimitiveArray::from_iter([2u32, 5, 10]);
274        let values = BoolArray::from(BitBuffer::from(vec![true, false, true]));
275        let decoded = runend_decode_bools(ends, values, 0, 10, &mut ctx)?;
276
277        let expected = BoolArray::from(BitBuffer::from(vec![
278            true, true, false, false, false, true, true, true, true, true,
279        ]));
280        assert_arrays_eq!(decoded, expected, &mut ctx);
281        Ok(())
282    }
283
284    #[test]
285    fn decode_bools_mostly_true() -> VortexResult<()> {
286        let mut ctx = SESSION.create_execution_ctx();
287        // Mostly true: [T, T, T, T, T, F, T, T, T, T]
288        let ends = PrimitiveArray::from_iter([5u32, 6, 10]);
289        let values = BoolArray::from(BitBuffer::from(vec![true, false, true]));
290        let decoded = runend_decode_bools(ends, values, 0, 10, &mut ctx)?;
291
292        let expected = BoolArray::from(BitBuffer::from(vec![
293            true, true, true, true, true, false, true, true, true, true,
294        ]));
295        assert_arrays_eq!(decoded, expected, &mut ctx);
296        Ok(())
297    }
298
299    #[test]
300    fn decode_bools_mostly_false() -> VortexResult<()> {
301        let mut ctx = SESSION.create_execution_ctx();
302        // Mostly false: [F, F, F, F, F, T, F, F, F, F]
303        let ends = PrimitiveArray::from_iter([5u32, 6, 10]);
304        let values = BoolArray::from(BitBuffer::from(vec![false, true, false]));
305        let decoded = runend_decode_bools(ends, values, 0, 10, &mut ctx)?;
306
307        let expected = BoolArray::from(BitBuffer::from(vec![
308            false, false, false, false, false, true, false, false, false, false,
309        ]));
310        assert_arrays_eq!(decoded, expected, &mut ctx);
311        Ok(())
312    }
313
314    #[test]
315    fn decode_bools_all_true_single_run() -> VortexResult<()> {
316        let mut ctx = SESSION.create_execution_ctx();
317        let ends = PrimitiveArray::from_iter([10u32]);
318        let values = BoolArray::from(BitBuffer::from(vec![true]));
319        let decoded = runend_decode_bools(ends, values, 0, 10, &mut ctx)?;
320
321        let expected = BoolArray::from(BitBuffer::from(vec![
322            true, true, true, true, true, true, true, true, true, true,
323        ]));
324        assert_arrays_eq!(decoded, expected, &mut ctx);
325        Ok(())
326    }
327
328    #[test]
329    fn decode_bools_all_false_single_run() -> VortexResult<()> {
330        let mut ctx = SESSION.create_execution_ctx();
331        let ends = PrimitiveArray::from_iter([10u32]);
332        let values = BoolArray::from(BitBuffer::from(vec![false]));
333        let decoded = runend_decode_bools(ends, values, 0, 10, &mut ctx)?;
334
335        let expected = BoolArray::from(BitBuffer::from(vec![
336            false, false, false, false, false, false, false, false, false, false,
337        ]));
338        assert_arrays_eq!(decoded, expected, &mut ctx);
339        Ok(())
340    }
341
342    #[test]
343    fn decode_bools_with_offset() -> VortexResult<()> {
344        let mut ctx = SESSION.create_execution_ctx();
345        // Test with offset: [T, T, F, F, F, T, T, T, T, T] -> slice [2..8] = [F, F, F, T, T, T]
346        let ends = PrimitiveArray::from_iter([2u32, 5, 10]);
347        let values = BoolArray::from(BitBuffer::from(vec![true, false, true]));
348        let decoded = runend_decode_bools(ends, values, 2, 6, &mut ctx)?;
349
350        let expected =
351            BoolArray::from(BitBuffer::from(vec![false, false, false, true, true, true]));
352        assert_arrays_eq!(decoded, expected, &mut ctx);
353        Ok(())
354    }
355
356    #[test]
357    fn decode_bools_nullable() -> VortexResult<()> {
358        use vortex_array::validity::Validity;
359
360        let mut ctx = SESSION.create_execution_ctx();
361        // 3 runs: T (valid), F (null), T (valid) -> [T, T, null, null, null, T, T, T, T, T]
362        let ends = PrimitiveArray::from_iter([2u32, 5, 10]);
363        let values = BoolArray::new(
364            BitBuffer::from(vec![true, false, true]),
365            Validity::from(BitBuffer::from(vec![true, false, true])),
366        );
367        let decoded = runend_decode_bools(ends, values, 0, 10, &mut ctx)?;
368
369        // Expected: values=[T, T, F, F, F, T, T, T, T, T], validity=[1, 1, 0, 0, 0, 1, 1, 1, 1, 1]
370        let expected = BoolArray::new(
371            BitBuffer::from(vec![
372                true, true, false, false, false, true, true, true, true, true,
373            ]),
374            Validity::from(BitBuffer::from(vec![
375                true, true, false, false, false, true, true, true, true, true,
376            ])),
377        );
378        assert_arrays_eq!(decoded, expected, &mut ctx);
379        Ok(())
380    }
381
382    #[test]
383    fn decode_bools_nullable_few_runs() -> VortexResult<()> {
384        let mut ctx = SESSION.create_execution_ctx();
385        // Test few runs (uses fast path): 5 runs of length 2000 each
386        let ends = PrimitiveArray::from_iter([2000u32, 4000, 6000, 8000, 10000]);
387        let values = BoolArray::new(
388            BitBuffer::from(vec![true, false, true, false, true]),
389            Validity::from(BitBuffer::from(vec![true, false, true, false, true])),
390        );
391        let decoded = runend_decode_bools(ends, values, 0, 10000, &mut ctx)?
392            .execute::<BoolArray>(&mut ctx)?;
393
394        // Check length and a few values
395        assert_eq!(decoded.len(), 10000);
396        // First run: valid true
397        assert!(
398            decoded
399                .as_ref()
400                .validity()?
401                .execute_mask(decoded.as_ref().len(), &mut ctx)?
402                .value(0)
403        );
404        assert!(decoded.to_bit_buffer().value(0));
405        // Second run: null (validity false)
406        assert!(
407            !decoded
408                .as_ref()
409                .validity()?
410                .execute_mask(decoded.as_ref().len(), &mut ctx)?
411                .value(2000)
412        );
413        // Third run: valid true
414        assert!(
415            decoded
416                .as_ref()
417                .validity()?
418                .execute_mask(decoded.as_ref().len(), &mut ctx)?
419                .value(4000)
420        );
421        assert!(decoded.to_bit_buffer().value(4000));
422        Ok(())
423    }
424}