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::IntoArray;
12use vortex_array::LEGACY_SESSION;
13use vortex_array::VortexSessionExecute;
14use vortex_array::arrays::BoolArray;
15use vortex_array::arrays::ConstantArray;
16use vortex_array::arrays::PrimitiveArray;
17use vortex_array::arrays::bool::BoolArrayExt;
18use vortex_array::dtype::DType;
19use vortex_array::dtype::Nullability;
20use vortex_array::match_each_unsigned_integer_ptype;
21use vortex_array::scalar::Scalar;
22use vortex_array::validity::Validity;
23use vortex_buffer::BitBuffer;
24use vortex_buffer::BitBufferMut;
25use vortex_error::VortexResult;
26use vortex_mask::Mask;
27
28use crate::iter::trimmed_ends_iter;
29
30/// Threshold for number of runs below which we use sequential append instead of prefill.
31/// With few runs, the overhead of prefilling the entire buffer dominates.
32const PREFILL_RUN_THRESHOLD: usize = 32;
33
34/// Decodes run-end encoded boolean values into a flat `BoolArray`.
35pub fn runend_decode_bools(
36    ends: PrimitiveArray,
37    values: BoolArray,
38    offset: usize,
39    length: usize,
40) -> VortexResult<ArrayRef> {
41    let validity = values.as_ref().validity()?.to_mask(
42        values.as_ref().len(),
43        &mut LEGACY_SESSION.create_execution_ctx(),
44    )?;
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 vortex_array::LEGACY_SESSION;
250    use vortex_array::ToCanonical;
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
260    use super::runend_decode_bools;
261
262    #[test]
263    fn decode_bools_alternating() -> VortexResult<()> {
264        // Alternating true/false: [T, T, F, F, F, T, T, T, T, T]
265        let ends = PrimitiveArray::from_iter([2u32, 5, 10]);
266        let values = BoolArray::from(BitBuffer::from(vec![true, false, true]));
267        let decoded = runend_decode_bools(ends, values, 0, 10)?;
268
269        let expected = BoolArray::from(BitBuffer::from(vec![
270            true, true, false, false, false, true, true, true, true, true,
271        ]));
272        assert_arrays_eq!(decoded, expected);
273        Ok(())
274    }
275
276    #[test]
277    fn decode_bools_mostly_true() -> VortexResult<()> {
278        // Mostly true: [T, T, T, T, T, F, T, T, T, T]
279        let ends = PrimitiveArray::from_iter([5u32, 6, 10]);
280        let values = BoolArray::from(BitBuffer::from(vec![true, false, true]));
281        let decoded = runend_decode_bools(ends, values, 0, 10)?;
282
283        let expected = BoolArray::from(BitBuffer::from(vec![
284            true, true, true, true, true, false, true, true, true, true,
285        ]));
286        assert_arrays_eq!(decoded, expected);
287        Ok(())
288    }
289
290    #[test]
291    fn decode_bools_mostly_false() -> VortexResult<()> {
292        // Mostly false: [F, F, F, F, F, T, F, F, F, F]
293        let ends = PrimitiveArray::from_iter([5u32, 6, 10]);
294        let values = BoolArray::from(BitBuffer::from(vec![false, true, false]));
295        let decoded = runend_decode_bools(ends, values, 0, 10)?;
296
297        let expected = BoolArray::from(BitBuffer::from(vec![
298            false, false, false, false, false, true, false, false, false, false,
299        ]));
300        assert_arrays_eq!(decoded, expected);
301        Ok(())
302    }
303
304    #[test]
305    fn decode_bools_all_true_single_run() -> VortexResult<()> {
306        let ends = PrimitiveArray::from_iter([10u32]);
307        let values = BoolArray::from(BitBuffer::from(vec![true]));
308        let decoded = runend_decode_bools(ends, values, 0, 10)?;
309
310        let expected = BoolArray::from(BitBuffer::from(vec![
311            true, true, true, true, true, true, true, true, true, true,
312        ]));
313        assert_arrays_eq!(decoded, expected);
314        Ok(())
315    }
316
317    #[test]
318    fn decode_bools_all_false_single_run() -> VortexResult<()> {
319        let ends = PrimitiveArray::from_iter([10u32]);
320        let values = BoolArray::from(BitBuffer::from(vec![false]));
321        let decoded = runend_decode_bools(ends, values, 0, 10)?;
322
323        let expected = BoolArray::from(BitBuffer::from(vec![
324            false, false, false, false, false, false, false, false, false, false,
325        ]));
326        assert_arrays_eq!(decoded, expected);
327        Ok(())
328    }
329
330    #[test]
331    fn decode_bools_with_offset() -> VortexResult<()> {
332        // Test with offset: [T, T, F, F, F, T, T, T, T, T] -> slice [2..8] = [F, F, F, T, T, T]
333        let ends = PrimitiveArray::from_iter([2u32, 5, 10]);
334        let values = BoolArray::from(BitBuffer::from(vec![true, false, true]));
335        let decoded = runend_decode_bools(ends, values, 2, 6)?;
336
337        let expected =
338            BoolArray::from(BitBuffer::from(vec![false, false, false, true, true, true]));
339        assert_arrays_eq!(decoded, expected);
340        Ok(())
341    }
342
343    #[test]
344    fn decode_bools_nullable() -> VortexResult<()> {
345        use vortex_array::validity::Validity;
346
347        // 3 runs: T (valid), F (null), T (valid) -> [T, T, null, null, null, T, T, T, T, T]
348        let ends = PrimitiveArray::from_iter([2u32, 5, 10]);
349        let values = BoolArray::new(
350            BitBuffer::from(vec![true, false, true]),
351            Validity::from(BitBuffer::from(vec![true, false, true])),
352        );
353        let decoded = runend_decode_bools(ends, values, 0, 10)?;
354
355        // Expected: values=[T, T, F, F, F, T, T, T, T, T], validity=[1, 1, 0, 0, 0, 1, 1, 1, 1, 1]
356        let expected = BoolArray::new(
357            BitBuffer::from(vec![
358                true, true, false, false, false, true, true, true, true, true,
359            ]),
360            Validity::from(BitBuffer::from(vec![
361                true, true, false, false, false, true, true, true, true, true,
362            ])),
363        );
364        assert_arrays_eq!(decoded, expected);
365        Ok(())
366    }
367
368    #[test]
369    fn decode_bools_nullable_few_runs() -> VortexResult<()> {
370        // Test few runs (uses fast path): 5 runs of length 2000 each
371        let ends = PrimitiveArray::from_iter([2000u32, 4000, 6000, 8000, 10000]);
372        let values = BoolArray::new(
373            BitBuffer::from(vec![true, false, true, false, true]),
374            Validity::from(BitBuffer::from(vec![true, false, true, false, true])),
375        );
376        let decoded = runend_decode_bools(ends, values, 0, 10000)?.to_bool();
377
378        // Check length and a few values
379        assert_eq!(decoded.len(), 10000);
380        // First run: valid true
381        assert!(
382            decoded
383                .as_ref()
384                .validity()?
385                .to_mask(
386                    decoded.as_ref().len(),
387                    &mut LEGACY_SESSION.create_execution_ctx()
388                )
389                .unwrap()
390                .value(0)
391        );
392        assert!(decoded.to_bit_buffer().value(0));
393        // Second run: null (validity false)
394        assert!(
395            !decoded
396                .as_ref()
397                .validity()?
398                .to_mask(
399                    decoded.as_ref().len(),
400                    &mut LEGACY_SESSION.create_execution_ctx()
401                )
402                .unwrap()
403                .value(2000)
404        );
405        // Third run: valid true
406        assert!(
407            decoded
408                .as_ref()
409                .validity()?
410                .to_mask(
411                    decoded.as_ref().len(),
412                    &mut LEGACY_SESSION.create_execution_ctx()
413                )
414                .unwrap()
415                .value(4000)
416        );
417        assert!(decoded.to_bit_buffer().value(4000));
418        Ok(())
419    }
420}