This is an automated email from the ASF dual-hosted git repository.

alamb pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/arrow-rs.git


The following commit(s) were added to refs/heads/main by this push:
     new cf744a6c94 perf(parquet): reuse `MaskSelection`'s cached selectors 
when converting to selectors (#10443)
cf744a6c94 is described below

commit cf744a6c945b0f27468dc7a2be73e799603bb2ed
Author: Huaijin <[email protected]>
AuthorDate: Tue Aug 4 01:34:26 2026 +0800

    perf(parquet): reuse `MaskSelection`'s cached selectors when converting to 
selectors (#10443)
    
    # Which issue does this PR close?
    
    - Closes #10422.
    
    # Rationale for this change
    
    Follow up to #10141. `RowSelection::iter()` caches the RLE form of a
    mask-backed selection, but the conversions to `RowSelector`s ignored
    that cache and re-encoded the bitmap, so a caller that iterates a
    selection and then consumes it encodes the whole bitmap twice.
    
    # What changes are included in this PR?
    
    - `MaskSelection::into_selectors` takes the cached `Vec` when it was
    populated and otherwise falls back to `mask_to_selectors`. Used by
    `RowSelection::into_selectors_vec` (which backs `From<RowSelection>` for
    `Vec<RowSelector>` and `VecDeque<RowSelector>`) and by `build_cursor`.
    - `MaskSelection::borrowed_selectors` does the same for `intersection`
    and `union`, returning `Cow::Borrowed` when the cache is populated and
    converting into a temporary when it is not, rather than populating the
    cache of a selection the caller still owns.
    - `mask_to_selectors` has no callers outside the `selection` module now,
    so its re-export is dropped.
    
    # Are these changes tested?
    
    Four new unit tests in `arrow_reader::selection::boolean` cover the
    cache being moved out rather than re-encoded (asserted on the `Vec`'s
    pointer), the cold-cache fallback, `borrowed_selectors` in both states,
    and `intersection`/`union` agreeing either way. `cargo test -p parquet
    --all-features` passes.
    
    `row_selector` gains two scenarios over a mask-backed selection of 300k
    rows: `mask_iterate_then_consume` iterates and then converts, as
    `ParquetAccessPlan::into_overall_row_selection` does, and `mask_consume`
    converts with a cold cache.
    
    ```
    cargo bench -p parquet --bench row_selector -- mask_
    ```
    
    | benchmark | main | this PR | change |
    | --- | --- | --- | --- |
    | `mask_iterate_then_consume/run01` | 2.237 ms | 1.202 ms | -46.5% |
    | `mask_iterate_then_consume/run04` | 272.5 µs | 132.4 µs | -51.4% |
    | `mask_iterate_then_consume/run16` | 73.7 µs | 32.5 µs | -55.9% |
    | `mask_iterate_then_consume/run64` | 26.0 µs | 13.6 µs | -47.8% |
    | `mask_iterate_then_consume/random` | 784.1 µs | 404.1 µs | -48.7% |
    
    `mask_consume` is unchanged: its medians over three runs per side land
    within 1.5% of `main`, inside the run-to-run spread `main` shows on its
    own.
    
    # Are there any user-facing changes?
    
    No, the added methods are internal.
---
 parquet/benches/row_selector.rs                    |  68 +++++++++++++-
 parquet/src/arrow/arrow_reader/read_plan.rs        |   4 +-
 .../src/arrow/arrow_reader/selection/boolean.rs    | 100 ++++++++++++++++++++-
 parquet/src/arrow/arrow_reader/selection/mod.rs    |  39 ++++----
 4 files changed, 184 insertions(+), 27 deletions(-)

diff --git a/parquet/benches/row_selector.rs b/parquet/benches/row_selector.rs
index 38fb7122ab..a29a1bf852 100644
--- a/parquet/benches/row_selector.rs
+++ b/parquet/benches/row_selector.rs
@@ -16,11 +16,16 @@
 // under the License.
 
 use arrow_array::BooleanArray;
+use arrow_buffer::BooleanBuffer;
 use criterion::*;
-use parquet::arrow::arrow_reader::RowSelection;
+use parquet::arrow::arrow_reader::{RowSelection, RowSelector};
 use rand::Rng;
 use std::hint;
 
+/// Run lengths for the mask conversion benchmarks. Shorter runs mean more
+/// [`RowSelector`]s per row, so the RLE encoding dominates.
+const MASK_RUN_LENGTHS: &[usize] = &[1, 4, 16, 32, 48, 64, 96, 128];
+
 /// Generates a random RowSelection with a specified selection ratio.
 ///
 /// # Arguments
@@ -39,6 +44,65 @@ fn generate_random_row_selection(total_rows: usize, 
selection_ratio: f64) -> Boo
     BooleanArray::from(bools)
 }
 
+/// Generates a mask alternating between selected and skipped runs of 
`run_len` rows.
+fn generate_run_length_mask(total_rows: usize, run_len: usize) -> 
BooleanBuffer {
+    BooleanBuffer::from_iter((0..total_rows).map(|row| (row / run_len) % 2 == 
0))
+}
+
+/// Benchmarks converting a mask-backed [`RowSelection`] into [`RowSelector`]s.
+///
+/// `RowSelection::iter` caches the RLE form, so a caller that iterates before
+/// consuming should reuse that cache rather than encode the bitmap twice.
+/// `mask_consume` is the same conversion with a cold cache.
+fn bench_mask_backed_conversion(c: &mut Criterion, total_rows: usize, 
selection_ratio: f64) {
+    let mut cases: Vec<(String, BooleanBuffer)> = MASK_RUN_LENGTHS
+        .iter()
+        .map(|&run_len| {
+            (
+                format!("run{run_len:02}"),
+                generate_run_length_mask(total_rows, run_len),
+            )
+        })
+        .collect();
+    cases.push((
+        "random".to_string(),
+        generate_random_row_selection(total_rows, selection_ratio)
+            .values()
+            .clone(),
+    ));
+
+    for (label, mask) in cases {
+        let selection = RowSelection::from_boolean_buffer(mask);
+
+        c.bench_with_input(
+            BenchmarkId::new("mask_iterate_then_consume", &label),
+            &selection,
+            |b, selection| {
+                b.iter(|| {
+                    // `clone` drops the selector cache, so each iteration
+                    // starts from an unconverted selection.
+                    let selection = selection.clone();
+                    let rows: usize = selection.iter().map(|s| 
s.row_count).sum();
+                    hint::black_box(rows);
+                    let selectors: Vec<RowSelector> = selection.into();
+                    hint::black_box(selectors);
+                })
+            },
+        );
+
+        c.bench_with_input(
+            BenchmarkId::new("mask_consume", &label),
+            &selection,
+            |b, selection| {
+                b.iter(|| {
+                    let selectors: Vec<RowSelector> = selection.clone().into();
+                    hint::black_box(selectors);
+                })
+            },
+        );
+    }
+}
+
 fn criterion_benchmark(c: &mut Criterion) {
     let total_rows = 300_000;
     let selection_ratio = 1.0 / 3.0;
@@ -82,6 +146,8 @@ fn criterion_benchmark(c: &mut Criterion) {
             hint::black_box(result);
         })
     });
+
+    bench_mask_backed_conversion(c, total_rows, selection_ratio);
 }
 
 criterion_group!(benches, criterion_benchmark);
diff --git a/parquet/src/arrow/arrow_reader/read_plan.rs 
b/parquet/src/arrow/arrow_reader/read_plan.rs
index 04f132e1bc..92ebbe9bae 100644
--- a/parquet/src/arrow/arrow_reader/read_plan.rs
+++ b/parquet/src/arrow/arrow_reader/read_plan.rs
@@ -20,7 +20,7 @@
 
 use crate::arrow::array_reader::ArrayReader;
 use crate::arrow::arrow_reader::selection::{
-    LoadedRowRanges, RowSelectionInner, RowSelectionPolicy, 
RowSelectionStrategy, mask_to_selectors,
+    LoadedRowRanges, RowSelectionInner, RowSelectionPolicy, 
RowSelectionStrategy,
 };
 use crate::arrow::arrow_reader::{
     ArrowPredicate, ParquetRecordBatchReader, RowSelection, 
RowSelectionCursor, RowSelector,
@@ -335,7 +335,7 @@ fn build_cursor(
             RowSelectionCursor::new_selectors(selectors)
         }
         (RowSelectionStrategy::Selectors, RowSelectionInner::Mask(mask)) => {
-            RowSelectionCursor::new_selectors(mask_to_selectors(mask.mask()))
+            RowSelectionCursor::new_selectors((*mask).into_selectors())
         }
     }
 }
diff --git a/parquet/src/arrow/arrow_reader/selection/boolean.rs 
b/parquet/src/arrow/arrow_reader/selection/boolean.rs
index 2ac879eba9..bf6c983d1c 100644
--- a/parquet/src/arrow/arrow_reader/selection/boolean.rs
+++ b/parquet/src/arrow/arrow_reader/selection/boolean.rs
@@ -26,6 +26,7 @@
 use super::RowSelector;
 use arrow_buffer::bit_iterator::BitSliceIterator;
 use arrow_buffer::{BooleanBuffer, BooleanBufferBuilder, Buffer};
+use std::borrow::Cow;
 use std::sync::OnceLock;
 
 /// Mask-backed [`RowSelection`] storage.
@@ -92,6 +93,22 @@ impl MaskSelection {
             .get_or_init(|| mask_to_selectors(&self.mask))
             .as_slice()
     }
+
+    /// Borrows the cached RLE form, converting into a temporary if not cached.
+    pub(super) fn borrowed_selectors(&self) -> Cow<'_, [RowSelector]> {
+        match self.selectors.get() {
+            Some(selectors) => Cow::Borrowed(selectors.as_slice()),
+            None => Cow::Owned(mask_to_selectors(&self.mask)),
+        }
+    }
+
+    /// The RLE form, taking the cache if it was populated.
+    pub(crate) fn into_selectors(self) -> Vec<RowSelector> {
+        match self.selectors.into_inner() {
+            Some(selectors) => selectors,
+            None => mask_to_selectors(&self.mask),
+        }
+    }
 }
 
 impl Clone for MaskSelection {
@@ -179,7 +196,7 @@ impl Iterator for MaskRunIter<'_> {
 }
 
 /// Materialize a [`BooleanBuffer`] into its RLE form.
-pub(crate) fn mask_to_selectors(mask: &BooleanBuffer) -> Vec<RowSelector> {
+pub(super) fn mask_to_selectors(mask: &BooleanBuffer) -> Vec<RowSelector> {
     let total_rows = mask.len();
     if total_rows == 0 {
         return Vec::new();
@@ -391,6 +408,87 @@ mod tests {
         );
     }
 
+    /// Enough runs that the RLE form is a real allocation, so the cache reuse
+    /// tests can track its pointer across the conversion.
+    fn interleaved_mask() -> BooleanBuffer {
+        BooleanBuffer::from((0..256).map(|i| i % 3 == 
0).collect::<Vec<bool>>())
+    }
+
+    fn cached_selectors_ptr(selection: &RowSelection) -> Option<*const 
RowSelector> {
+        match &selection.inner {
+            RowSelectionInner::Mask(m) => m.selectors.get().map(|s| 
s.as_ptr()),
+            _ => unreachable!(),
+        }
+    }
+
+    #[test]
+    fn test_into_selectors_takes_the_iter_cache() {
+        let selection = RowSelection::from_boolean_buffer(interleaved_mask());
+        let expected: Vec<RowSelector> = selection.iter().copied().collect();
+
+        let cached_ptr = cached_selectors_ptr(&selection).expect("iter 
populates the cache");
+        let selectors: Vec<RowSelector> = selection.into();
+
+        assert_eq!(selectors, expected);
+        // Moved out of the cache rather than re-encoded from the bitmap.
+        assert_eq!(selectors.as_ptr(), cached_ptr);
+    }
+
+    #[test]
+    fn test_into_selectors_without_cache_still_converts() {
+        let selection = RowSelection::from_boolean_buffer(interleaved_mask());
+        assert!(cached_selectors_ptr(&selection).is_none());
+
+        let selectors: Vec<RowSelector> = selection.into();
+        assert_eq!(selectors, mask_to_selectors(&interleaved_mask()));
+
+        // `VecDeque` goes through the same path.
+        let selection = RowSelection::from_boolean_buffer(interleaved_mask());
+        let _ = selection.iter().count();
+        let deque: std::collections::VecDeque<RowSelector> = selection.into();
+        assert_eq!(Vec::from(deque), selectors);
+    }
+
+    #[test]
+    fn test_borrowed_selectors_reuses_cache_without_populating_it() {
+        let selection = RowSelection::from_boolean_buffer(interleaved_mask());
+        let mask = match &selection.inner {
+            RowSelectionInner::Mask(m) => m,
+            _ => unreachable!(),
+        };
+
+        // Uncached: converts into a temporary, leaving the cache empty.
+        assert!(matches!(mask.borrowed_selectors(), Cow::Owned(_)));
+        assert!(mask.selectors.get().is_none());
+
+        let expected: Vec<RowSelector> = selection.iter().copied().collect();
+        let mask = match &selection.inner {
+            RowSelectionInner::Mask(m) => m,
+            _ => unreachable!(),
+        };
+        match mask.borrowed_selectors() {
+            Cow::Borrowed(selectors) => assert_eq!(selectors, 
expected.as_slice()),
+            Cow::Owned(_) => panic!("expected the cached selectors to be 
reused"),
+        }
+    }
+
+    #[test]
+    fn test_set_algebra_agrees_whether_or_not_the_cache_is_populated() {
+        let bits: Vec<bool> = (0..256).map(|i| i % 3 == 0).collect();
+        let other: RowSelection = 
RowSelection::from_filters(&[BooleanArray::from(
+            (0..256).map(|i| i % 5 != 0).collect::<Vec<bool>>(),
+        )]);
+
+        let cold = 
RowSelection::from_boolean_buffer(BooleanBuffer::from(bits.clone()));
+        let warm = 
RowSelection::from_boolean_buffer(BooleanBuffer::from(bits));
+        let _ = warm.iter().count();
+
+        assert_eq!(cold.intersection(&other), warm.intersection(&other));
+        assert_eq!(other.intersection(&cold), other.intersection(&warm));
+        assert_eq!(cold.union(&other), warm.union(&other));
+        assert_eq!(other.union(&cold), other.union(&warm));
+    }
+
     #[test]
     fn test_mask_run_iter_streams_without_cache() {
         let selection = 
RowSelection::from_boolean_buffer(BooleanBuffer::from(vec![
diff --git a/parquet/src/arrow/arrow_reader/selection/mod.rs 
b/parquet/src/arrow/arrow_reader/selection/mod.rs
index ff8d5e9a61..517022f173 100644
--- a/parquet/src/arrow/arrow_reader/selection/mod.rs
+++ b/parquet/src/arrow/arrow_reader/selection/mod.rs
@@ -48,7 +48,6 @@ use algebra::{
     intersect_row_selections, union_masks, union_row_selections,
 };
 pub use boolean::MaskRunIter;
-pub(crate) use boolean::mask_to_selectors;
 use boolean::{
     MaskSelection, limit_mask, mask_has_at_least_runs, offset_mask, 
split_off_mask, trim_mask,
 };
@@ -300,7 +299,7 @@ impl RowSelection {
     fn into_selectors_vec(self) -> Vec<RowSelector> {
         match self.inner {
             RowSelectionInner::Selectors(s) => s,
-            RowSelectionInner::Mask(m) => mask_to_selectors(m.mask()),
+            RowSelectionInner::Mask(m) => (*m).into_selectors(),
         }
     }
 
@@ -489,12 +488,10 @@ impl RowSelection {
                 intersect_row_selections(l, r)
             }
             (RowSelectionInner::Selectors(l), RowSelectionInner::Mask(r)) => {
-                let r = mask_to_selectors(r.mask());
-                intersect_row_selections(l, &r)
+                intersect_row_selections(l, &r.borrowed_selectors())
             }
             (RowSelectionInner::Mask(l), RowSelectionInner::Selectors(r)) => {
-                let l = mask_to_selectors(l.mask());
-                intersect_row_selections(&l, r)
+                intersect_row_selections(&l.borrowed_selectors(), r)
             }
         }
     }
@@ -506,23 +503,19 @@ impl RowSelection {
     ///
     /// returned:  NYYYYYNNYYNYN
     pub fn union(&self, other: &Self) -> Self {
-        match &self.inner {
-            RowSelectionInner::Mask(l) => match &other.inner {
-                RowSelectionInner::Mask(r) => {
-                    Self::from_boolean_buffer(union_masks(l.mask(), r.mask()))
-                }
-                RowSelectionInner::Selectors(r) => {
-                    let l = mask_to_selectors(l.mask());
-                    union_row_selections(&l, r)
-                }
-            },
-            RowSelectionInner::Selectors(l) => match &other.inner {
-                RowSelectionInner::Mask(r) => {
-                    let r = mask_to_selectors(r.mask());
-                    union_row_selections(l, &r)
-                }
-                RowSelectionInner::Selectors(r) => union_row_selections(l, r),
-            },
+        match (&self.inner, &other.inner) {
+            (RowSelectionInner::Mask(l), RowSelectionInner::Mask(r)) => {
+                Self::from_boolean_buffer(union_masks(l.mask(), r.mask()))
+            }
+            (RowSelectionInner::Selectors(l), RowSelectionInner::Selectors(r)) 
=> {
+                union_row_selections(l, r)
+            }
+            (RowSelectionInner::Selectors(l), RowSelectionInner::Mask(r)) => {
+                union_row_selections(l, &r.borrowed_selectors())
+            }
+            (RowSelectionInner::Mask(l), RowSelectionInner::Selectors(r)) => {
+                union_row_selections(&l.borrowed_selectors(), r)
+            }
         }
     }
 

Reply via email to