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 03b652ea7a bench(parquet): Add benchmarks for mask-backed
`RowSelection::and_then` (#10969)
03b652ea7a is described below
commit 03b652ea7a8e6d57f9e536d678a1ea2825458423
Author: Huaijin <[email protected]>
AuthorDate: Fri Sep 25 03:12:49 2026 +0800
bench(parquet): Add benchmarks for mask-backed `RowSelection::and_then`
(#10969)
# Which issue does this PR close?
- Part of #10967.
# Rationale for this change
Add focused benchmarks for mask-backed `RowSelection::and_then`. These
benchmarks measure the input shapes targeted by #10968 and make its
performance results reproducible.
# What changes are included in this PR?
- Add pseudo-random and clustered mask generators.
- Sweep outer and inner selectivity around the proposed thresholds.
- Sweep input lengths on both sides of the 8,192-row threshold.
- Cover non-zero bit offsets in both operands.
The benchmark registers 64 cases covering threshold boundaries and
representative small, large, sparse, and dense inputs.
# Are these changes tested?
Yes. The benchmark compiles on `main`, and Criterion lists all 64 new
cases.
```console
cargo bench -p parquet --bench row_selector --no-run
cargo bench -p parquet --bench row_selector -- mask_and_then_ --list
```
# Are there any user-facing changes?
No.
---
parquet/benches/row_selector.rs | 203 +++++++++++++++++++++++++++++++++++++++-
1 file changed, 202 insertions(+), 1 deletion(-)
diff --git a/parquet/benches/row_selector.rs b/parquet/benches/row_selector.rs
index 5d8abd3281..3f41f2426f 100644
--- a/parquet/benches/row_selector.rs
+++ b/parquet/benches/row_selector.rs
@@ -20,7 +20,7 @@ use arrow_buffer::BooleanBuffer;
use criterion::*;
use parquet::arrow::arrow_reader::{RowSelection, RowSelector};
use rand::RngExt;
-use std::hint;
+use std::{hint, time::Duration};
/// Run lengths for the mask conversion benchmarks. Shorter runs mean more
/// [`RowSelector`]s per row, so the RLE encoding dominates.
@@ -28,6 +28,19 @@ const MASK_RUN_LENGTHS: &[usize] = &[1, 4, 16, 32, 48, 64,
96, 128];
const MASK_ALGEBRA_ROWS: usize = 3_000_000;
+/// Inputs for the mask-backed `and_then` sweeps. `MASK_AND_THEN_ROWS` is the
+/// length used by the selectivity and offset sweeps. Selectivities are the
+/// percentage of rows that are selected; the values bracket the thresholds
+/// that choose between the word-at-a-time expansion and the set-index
+/// implementation (roughly 75% outer, 5% inner, 8,192 rows), and the lengths
+/// include sizes on both sides of the row threshold.
+const MASK_AND_THEN_ROWS: usize = 8192;
+const MASK_AND_THEN_OUTER_SELECTIVITY: &[usize] = &[70, 75, 80, 99];
+const MASK_AND_THEN_INNER_SELECTIVITY: &[usize] = &[1, 5, 10, 99];
+const MASK_AND_THEN_LENGTHS: &[usize] = &[64, 4096, 8192, 16_384, 65_536];
+const MASK_AND_THEN_OFFSETS: &[(&str, usize, usize)] =
+ &[("outer_3_inner_5", 3, 5), ("outer_7_inner_1", 7, 1)];
+
/// Operand length pairs. Unequal lengths pass the longer side's tail through
unchanged,
/// so the ratio decides how much of the work is the bitwise combine versus
the tail.
const MASK_ALGEBRA_LENGTHS: &[(&str, usize, usize)] = &[
@@ -79,6 +92,192 @@ fn mask_algebra_operand(len: usize, offset: usize,
selection_ratio: f64) -> RowS
RowSelection::from_boolean_buffer(BooleanBuffer::from(bits).slice(offset,
len))
}
+/// Selects exactly `selection_percent` of `len` rows, chosen by a hash of the
+/// row index so the set bits are spread evenly. The buffer is built with
+/// `offset` leading bits and then sliced, so the mask starts mid-word.
+fn pseudo_random_mask_with_offset(
+ len: usize,
+ selection_percent: usize,
+ seed: u64,
+ offset: usize,
+) -> BooleanBuffer {
+ assert!(len > 1);
+ assert!(selection_percent <= 100);
+ let selected = (len * selection_percent / 100).clamp(1, len - 1);
+ let mut ranked = (0..len)
+ .map(|row| {
+ let mut value = (row as u64).wrapping_add(seed);
+ value = (value ^ (value >>
30)).wrapping_mul(0xbf58_476d_1ce4_e5b9);
+ value = (value ^ (value >>
27)).wrapping_mul(0x94d0_49bb_1331_11eb);
+ value ^= value >> 31;
+ (value, row)
+ })
+ .collect::<Vec<_>>();
+ ranked.select_nth_unstable_by_key(selected, |(value, _)| *value);
+
+ let mut bits = vec![false; len + offset];
+ for (_, row) in &ranked[..selected] {
+ bits[offset + *row] = true;
+ }
+ BooleanBuffer::from(bits).slice(offset, len)
+}
+
+/// Selects `selection_percent` of `len` rows as one contiguous run starting at
+/// `seed % len` and wrapping around the end. Every word is all set or all
+/// unset except at the run boundaries, which models page-level filters.
+fn clustered_mask(len: usize, selection_percent: usize, seed: usize) ->
BooleanBuffer {
+ assert!(len > 1);
+ assert!(selection_percent <= 100);
+ let selected = (len * selection_percent / 100).clamp(1, len - 1);
+ let start = seed % len;
+ let mut bits = vec![false; len];
+ for row in 0..selected {
+ bits[(start + row) % len] = true;
+ }
+ BooleanBuffer::from(bits)
+}
+
+/// Builds a pseudo-random outer mask of `rows` rows and an inner mask sized to
+/// the outer mask's selected row count, as `and_then` requires.
+fn mask_and_then_operands(
+ rows: usize,
+ outer_percent: usize,
+ inner_percent: usize,
+) -> (RowSelection, RowSelection) {
+ mask_and_then_operands_with_offsets(rows, outer_percent, inner_percent, 0,
0)
+}
+
+fn mask_and_then_operands_with_offsets(
+ rows: usize,
+ outer_percent: usize,
+ inner_percent: usize,
+ outer_offset: usize,
+ inner_offset: usize,
+) -> (RowSelection, RowSelection) {
+ let outer =
+ pseudo_random_mask_with_offset(rows, outer_percent,
0x517c_c1b7_2722_0a95, outer_offset);
+ let inner = pseudo_random_mask_with_offset(
+ outer.count_set_bits(),
+ inner_percent,
+ 0x6eed_0e9d_a4d9_4a4f,
+ inner_offset,
+ );
+ (
+ RowSelection::from_boolean_buffer(outer),
+ RowSelection::from_boolean_buffer(inner),
+ )
+}
+
+/// Same as `mask_and_then_operands`, but both masks are contiguous runs.
+fn clustered_mask_and_then_operands(
+ rows: usize,
+ outer_percent: usize,
+ inner_percent: usize,
+) -> (RowSelection, RowSelection) {
+ let outer = clustered_mask(rows, outer_percent, 17);
+ let inner = clustered_mask(outer.count_set_bits(), inner_percent, 31);
+ (
+ RowSelection::from_boolean_buffer(outer),
+ RowSelection::from_boolean_buffer(inner),
+ )
+}
+
+/// Sweeps the inputs that decide whether the word-at-a-time mask expansion is
+/// faster than the set-index implementation. Short measurement times keep the
+/// full matrix practical while still providing many iterations per sample.
+fn bench_mask_and_then_sweeps(c: &mut Criterion) {
+ let mut selectivity = c.benchmark_group("mask_and_then_selectivity/8192");
+ selectivity
+ .warm_up_time(Duration::from_millis(500))
+ .measurement_time(Duration::from_secs(1))
+ .sample_size(30);
+
+ for &outer_percent in MASK_AND_THEN_OUTER_SELECTIVITY {
+ for &inner_percent in MASK_AND_THEN_INNER_SELECTIVITY {
+ let (outer, inner) =
+ mask_and_then_operands(MASK_AND_THEN_ROWS, outer_percent,
inner_percent);
+ selectivity.bench_function(
+ format!("outer_{outer_percent:02}_inner_{inner_percent:02}"),
+ |b| b.iter(|| hint::black_box(outer.and_then(&inner))),
+ );
+ }
+ }
+ selectivity.finish();
+
+ let mut clustered =
c.benchmark_group("mask_and_then_clustered_selectivity/8192");
+ clustered
+ .warm_up_time(Duration::from_millis(500))
+ .measurement_time(Duration::from_secs(1))
+ .sample_size(30);
+
+ for &outer_percent in MASK_AND_THEN_OUTER_SELECTIVITY {
+ for &inner_percent in MASK_AND_THEN_INNER_SELECTIVITY {
+ let (outer, inner) =
+ clustered_mask_and_then_operands(MASK_AND_THEN_ROWS,
outer_percent, inner_percent);
+ clustered.bench_function(
+ format!("outer_{outer_percent:02}_inner_{inner_percent:02}"),
+ |b| b.iter(|| hint::black_box(outer.and_then(&inner))),
+ );
+ }
+ }
+ clustered.finish();
+
+ let mut offsets =
c.benchmark_group("mask_and_then_offsets/8192/outer_75_inner_05");
+ offsets
+ .warm_up_time(Duration::from_millis(500))
+ .measurement_time(Duration::from_secs(1))
+ .sample_size(30);
+
+ for &(label, outer_offset, inner_offset) in MASK_AND_THEN_OFFSETS {
+ let (outer, inner) = mask_and_then_operands_with_offsets(
+ MASK_AND_THEN_ROWS,
+ 75,
+ 5,
+ outer_offset,
+ inner_offset,
+ );
+ offsets.bench_function(label, |b| {
+ b.iter(|| hint::black_box(outer.and_then(&inner)))
+ });
+ }
+ offsets.finish();
+
+ for &(outer_percent, inner_percent) in &[(75, 5), (99, 5), (99, 99)] {
+ let mut lengths = c.benchmark_group(format!(
+
"mask_and_then_length/outer_{outer_percent:02}_inner_{inner_percent:02}"
+ ));
+ lengths
+ .warm_up_time(Duration::from_millis(500))
+ .measurement_time(Duration::from_secs(1))
+ .sample_size(30);
+
+ for &rows in MASK_AND_THEN_LENGTHS {
+ let (outer, inner) = mask_and_then_operands(rows, outer_percent,
inner_percent);
+ lengths.bench_function(rows.to_string(), |b| {
+ b.iter(|| hint::black_box(outer.and_then(&inner)))
+ });
+ }
+ lengths.finish();
+
+ let mut clustered_lengths = c.benchmark_group(format!(
+
"mask_and_then_clustered_length/outer_{outer_percent:02}_inner_{inner_percent:02}"
+ ));
+ clustered_lengths
+ .warm_up_time(Duration::from_millis(500))
+ .measurement_time(Duration::from_secs(1))
+ .sample_size(30);
+
+ for &rows in MASK_AND_THEN_LENGTHS {
+ let (outer, inner) =
+ clustered_mask_and_then_operands(rows, outer_percent,
inner_percent);
+ clustered_lengths.bench_function(rows.to_string(), |b| {
+ b.iter(|| hint::black_box(outer.and_then(&inner)))
+ });
+ }
+ clustered_lengths.finish();
+ }
+}
+
/// Benchmarks the bitwise `intersection`/`union` path, taken when both
operands are
/// mask-backed. The `intersection`/`union` benchmarks above are
selector-backed and take
/// the [`RowSelector`] merge path instead.
@@ -202,6 +401,8 @@ fn criterion_benchmark(c: &mut Criterion) {
})
});
+ bench_mask_and_then_sweeps(c);
+
bench_mask_backed_algebra(c, selection_ratio);
bench_mask_backed_conversion(c, total_rows, selection_ratio);
}