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#[allow(clippy::inline_always)]
105#[inline(always)]
106fn decode_few_runs_no_offset<E: vortex_array::dtype::IntegerPType>(
107    ends: &[E],
108    values: &BitBuffer,
109    validity: Mask,
110    nullability: Nullability,
111    length: usize,
112) -> ArrayRef {
113    match validity {
114        Mask::AllTrue(_) => {
115            let mut decoded = BitBufferMut::with_capacity(length);
116            let mut prev_end = 0usize;
117            for (i, &end) in ends.iter().enumerate() {
118                let end = end.as_().min(length);
119                decoded.append_n(values.value(i), end - prev_end);
120                prev_end = end;
121            }
122            BoolArray::new(decoded.freeze(), nullability.into()).into_array()
123        }
124        Mask::AllFalse(_) => {
125            ConstantArray::new(Scalar::null(DType::Bool(Nullability::Nullable)), length)
126                .into_array()
127        }
128        Mask::Values(mask) => {
129            let validity_buf = mask.bit_buffer();
130            let mut decoded = BitBufferMut::with_capacity(length);
131            let mut decoded_validity = BitBufferMut::with_capacity(length);
132            let mut prev_end = 0usize;
133            for (i, &end) in ends.iter().enumerate() {
134                let end = end.as_().min(length);
135                let run_len = end - prev_end;
136                let is_valid = validity_buf.value(i);
137                if is_valid {
138                    decoded_validity.append_n(true, run_len);
139                    decoded.append_n(values.value(i), run_len);
140                } else {
141                    decoded_validity.append_n(false, run_len);
142                    decoded.append_n(false, run_len);
143                }
144                prev_end = end;
145            }
146            BoolArray::new(decoded.freeze(), Validity::from(decoded_validity.freeze())).into_array()
147        }
148    }
149}
150
151/// Decodes run-end encoded booleans when all values are valid (non-nullable).
152fn decode_bool_non_nullable(
153    run_ends: impl Iterator<Item = usize>,
154    values: &BitBuffer,
155    nullability: Nullability,
156    length: usize,
157) -> BoolArray {
158    let num_runs = values.len();
159
160    // For few runs, sequential append is faster than prefill + modify
161    if num_runs < PREFILL_RUN_THRESHOLD {
162        let mut decoded = BitBufferMut::with_capacity(length);
163        for (end, value) in run_ends.zip(values.iter()) {
164            decoded.append_n(value, end - decoded.len());
165        }
166        return BoolArray::new(decoded.freeze(), nullability.into());
167    }
168
169    // Adaptive strategy: prefill with majority value, only flip minority runs
170    let prefill = values.true_count() > num_runs - values.true_count();
171    let mut decoded = BitBufferMut::full(prefill, length);
172    let mut current_pos = 0usize;
173
174    for (end, value) in run_ends.zip_eq(values.iter()) {
175        if end > current_pos && value != prefill {
176            // SAFETY: current_pos < end <= length == decoded.len()
177            unsafe { decoded.fill_range_unchecked(current_pos, end, value) };
178        }
179        current_pos = end;
180    }
181    BoolArray::new(decoded.freeze(), nullability.into())
182}
183
184/// Decodes run-end encoded booleans when values may be null (nullable).
185fn decode_bool_nullable(
186    run_ends: impl Iterator<Item = usize>,
187    values: &BitBuffer,
188    validity_mask: &BitBuffer,
189    length: usize,
190) -> BoolArray {
191    let num_runs = values.len();
192
193    // For few runs, sequential append is faster than prefill + modify
194    if num_runs < PREFILL_RUN_THRESHOLD {
195        return decode_nullable_sequential(run_ends, values, validity_mask, length);
196    }
197
198    // Adaptive strategy: prefill each buffer with its majority value
199    let prefill_decoded = values.true_count() > num_runs - values.true_count();
200    let prefill_valid = validity_mask.true_count() > num_runs - validity_mask.true_count();
201
202    let mut decoded = BitBufferMut::full(prefill_decoded, length);
203    let mut decoded_validity = BitBufferMut::full(prefill_valid, length);
204    let mut current_pos = 0usize;
205
206    for (end, (value, is_valid)) in run_ends.zip_eq(values.iter().zip(validity_mask.iter())) {
207        if end > current_pos {
208            // SAFETY: current_pos < end <= length == decoded.len() == decoded_validity.len()
209            if is_valid != prefill_valid {
210                unsafe { decoded_validity.fill_range_unchecked(current_pos, end, is_valid) };
211            }
212            // Decoded bit should be the actual value when valid, false when null.
213            let want_decoded = is_valid && value;
214            if want_decoded != prefill_decoded {
215                unsafe { decoded.fill_range_unchecked(current_pos, end, want_decoded) };
216            }
217            current_pos = end;
218        }
219    }
220    BoolArray::new(decoded.freeze(), Validity::from(decoded_validity.freeze()))
221}
222
223/// Sequential decode for few runs - avoids prefill overhead.
224#[allow(clippy::inline_always)]
225#[inline(always)]
226fn decode_nullable_sequential(
227    run_ends: impl Iterator<Item = usize>,
228    values: &BitBuffer,
229    validity_mask: &BitBuffer,
230    length: usize,
231) -> BoolArray {
232    let mut decoded = BitBufferMut::with_capacity(length);
233    let mut decoded_validity = BitBufferMut::with_capacity(length);
234
235    for (end, (value, is_valid)) in run_ends.zip(values.iter().zip(validity_mask.iter())) {
236        let run_len = end - decoded.len();
237        if is_valid {
238            decoded_validity.append_n(true, run_len);
239            decoded.append_n(value, run_len);
240        } else {
241            decoded_validity.append_n(false, run_len);
242            decoded.append_n(false, run_len);
243        }
244    }
245
246    BoolArray::new(decoded.freeze(), Validity::from(decoded_validity.freeze()))
247}
248
249#[cfg(test)]
250mod tests {
251    use std::sync::LazyLock;
252
253    use vortex_array::VortexSessionExecute;
254    use vortex_array::arrays::BoolArray;
255    use vortex_array::arrays::PrimitiveArray;
256    use vortex_array::arrays::bool::BoolArrayExt;
257    use vortex_array::assert_arrays_eq;
258    use vortex_array::validity::Validity;
259    use vortex_buffer::BitBuffer;
260    use vortex_error::VortexResult;
261    use vortex_session::VortexSession;
262
263    use super::runend_decode_bools;
264
265    static SESSION: LazyLock<VortexSession> = LazyLock::new(|| {
266        let session = vortex_array::array_session();
267        crate::initialize(&session);
268        session
269    });
270
271    #[test]
272    fn decode_bools_alternating() -> VortexResult<()> {
273        let mut ctx = SESSION.create_execution_ctx();
274        // Alternating true/false: [T, T, F, F, F, T, T, T, T, T]
275        let ends = PrimitiveArray::from_iter([2u32, 5, 10]);
276        let values = BoolArray::from(BitBuffer::from(vec![true, false, true]));
277        let decoded = runend_decode_bools(ends, values, 0, 10, &mut ctx)?;
278
279        let expected = BoolArray::from(BitBuffer::from(vec![
280            true, true, false, false, false, true, true, true, true, true,
281        ]));
282        assert_arrays_eq!(decoded, expected, &mut ctx);
283        Ok(())
284    }
285
286    #[test]
287    fn decode_bools_mostly_true() -> VortexResult<()> {
288        let mut ctx = SESSION.create_execution_ctx();
289        // Mostly true: [T, T, T, T, T, F, T, T, T, T]
290        let ends = PrimitiveArray::from_iter([5u32, 6, 10]);
291        let values = BoolArray::from(BitBuffer::from(vec![true, false, true]));
292        let decoded = runend_decode_bools(ends, values, 0, 10, &mut ctx)?;
293
294        let expected = BoolArray::from(BitBuffer::from(vec![
295            true, true, true, true, true, false, true, true, true, true,
296        ]));
297        assert_arrays_eq!(decoded, expected, &mut ctx);
298        Ok(())
299    }
300
301    #[test]
302    fn decode_bools_mostly_false() -> VortexResult<()> {
303        let mut ctx = SESSION.create_execution_ctx();
304        // Mostly false: [F, F, F, F, F, T, F, F, F, F]
305        let ends = PrimitiveArray::from_iter([5u32, 6, 10]);
306        let values = BoolArray::from(BitBuffer::from(vec![false, true, false]));
307        let decoded = runend_decode_bools(ends, values, 0, 10, &mut ctx)?;
308
309        let expected = BoolArray::from(BitBuffer::from(vec![
310            false, false, false, false, false, true, false, false, false, false,
311        ]));
312        assert_arrays_eq!(decoded, expected, &mut ctx);
313        Ok(())
314    }
315
316    #[test]
317    fn decode_bools_all_true_single_run() -> VortexResult<()> {
318        let mut ctx = SESSION.create_execution_ctx();
319        let ends = PrimitiveArray::from_iter([10u32]);
320        let values = BoolArray::from(BitBuffer::from(vec![true]));
321        let decoded = runend_decode_bools(ends, values, 0, 10, &mut ctx)?;
322
323        let expected = BoolArray::from(BitBuffer::from(vec![
324            true, true, true, true, true, true, true, true, true, true,
325        ]));
326        assert_arrays_eq!(decoded, expected, &mut ctx);
327        Ok(())
328    }
329
330    #[test]
331    fn decode_bools_all_false_single_run() -> VortexResult<()> {
332        let mut ctx = SESSION.create_execution_ctx();
333        let ends = PrimitiveArray::from_iter([10u32]);
334        let values = BoolArray::from(BitBuffer::from(vec![false]));
335        let decoded = runend_decode_bools(ends, values, 0, 10, &mut ctx)?;
336
337        let expected = BoolArray::from(BitBuffer::from(vec![
338            false, false, false, false, false, false, false, false, false, false,
339        ]));
340        assert_arrays_eq!(decoded, expected, &mut ctx);
341        Ok(())
342    }
343
344    #[test]
345    fn decode_bools_with_offset() -> VortexResult<()> {
346        let mut ctx = SESSION.create_execution_ctx();
347        // Test with offset: [T, T, F, F, F, T, T, T, T, T] -> slice [2..8] = [F, F, F, T, T, T]
348        let ends = PrimitiveArray::from_iter([2u32, 5, 10]);
349        let values = BoolArray::from(BitBuffer::from(vec![true, false, true]));
350        let decoded = runend_decode_bools(ends, values, 2, 6, &mut ctx)?;
351
352        let expected =
353            BoolArray::from(BitBuffer::from(vec![false, false, false, true, true, true]));
354        assert_arrays_eq!(decoded, expected, &mut ctx);
355        Ok(())
356    }
357
358    #[test]
359    fn decode_bools_nullable() -> VortexResult<()> {
360        use vortex_array::validity::Validity;
361
362        let mut ctx = SESSION.create_execution_ctx();
363        // 3 runs: T (valid), F (null), T (valid) -> [T, T, null, null, null, T, T, T, T, T]
364        let ends = PrimitiveArray::from_iter([2u32, 5, 10]);
365        let values = BoolArray::new(
366            BitBuffer::from(vec![true, false, true]),
367            Validity::from(BitBuffer::from(vec![true, false, true])),
368        );
369        let decoded = runend_decode_bools(ends, values, 0, 10, &mut ctx)?;
370
371        // Expected: values=[T, T, F, F, F, T, T, T, T, T], validity=[1, 1, 0, 0, 0, 1, 1, 1, 1, 1]
372        let expected = BoolArray::new(
373            BitBuffer::from(vec![
374                true, true, false, false, false, true, true, true, true, true,
375            ]),
376            Validity::from(BitBuffer::from(vec![
377                true, true, false, false, false, true, true, true, true, true,
378            ])),
379        );
380        assert_arrays_eq!(decoded, expected, &mut ctx);
381        Ok(())
382    }
383
384    #[test]
385    fn decode_bools_nullable_few_runs() -> VortexResult<()> {
386        let mut ctx = SESSION.create_execution_ctx();
387        // Test few runs (uses fast path): 5 runs of length 2000 each
388        let ends = PrimitiveArray::from_iter([2000u32, 4000, 6000, 8000, 10000]);
389        let values = BoolArray::new(
390            BitBuffer::from(vec![true, false, true, false, true]),
391            Validity::from(BitBuffer::from(vec![true, false, true, false, true])),
392        );
393        let decoded = runend_decode_bools(ends, values, 0, 10000, &mut ctx)?
394            .execute::<BoolArray>(&mut ctx)?;
395
396        // Check length and a few values
397        assert_eq!(decoded.len(), 10000);
398        // First run: valid true
399        assert!(
400            decoded
401                .as_ref()
402                .validity()?
403                .execute_mask(decoded.as_ref().len(), &mut ctx)?
404                .value(0)
405        );
406        assert!(decoded.to_bit_buffer().value(0));
407        // Second run: null (validity false)
408        assert!(
409            !decoded
410                .as_ref()
411                .validity()?
412                .execute_mask(decoded.as_ref().len(), &mut ctx)?
413                .value(2000)
414        );
415        // Third run: valid true
416        assert!(
417            decoded
418                .as_ref()
419                .validity()?
420                .execute_mask(decoded.as_ref().len(), &mut ctx)?
421                .value(4000)
422        );
423        assert!(decoded.to_bit_buffer().value(4000));
424        Ok(())
425    }
426}