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