Skip to content

feat(parquet): RowSelection can be backed by a BooleanBuffer - #10141

Merged
alamb merged 34 commits into
apache:mainfrom
haohuaijin:export-mask
Jul 24, 2026
Merged

feat(parquet): RowSelection can be backed by a BooleanBuffer#10141
alamb merged 34 commits into
apache:mainfrom
haohuaijin:export-mask

Conversation

@haohuaijin

@haohuaijin haohuaijin commented Jun 14, 2026

Copy link
Copy Markdown
Contributor

Which issue does this PR close?

Rationale for this change

RowSelection currently stores selections as Vec<RowSelector> (16 bytes per selector). This is compact for long runs, but expensive for scattered matches. With ~35% isolated single-row hits, it uses about 11.2 bytes per input row. A BooleanBuffer uses 1 bit per input row, about 90x less memory.

The reader can also choose the Mask strategy, which converts selectors back into a bitmap. When the caller already had a bitmap, this conversion round-trip is unnecessary.

This PR lets RowSelection preserve a caller-provided bitmap and pass it directly to mask execution.

This is not intended to claim broad DataFusion / TPC-DS / ClickBench speedups. Current common DataFusion SQL paths generally do not naturally produce bitmap-backed RowSelections. The practical benefit is for integrations that already have a row-level bitmap and need Parquet to consume it without materializing a large selector list.

What changes are included in this PR?

RowSelection can now be backed by either Vec<RowSelector> or BooleanBuffer. New public construction:

pub fn RowSelection::from_boolean_buffer(mask: BooleanBuffer) -> Self;
impl From<BooleanBuffer> for RowSelection;

Methods that can work directly on the bitmap now do so:

  • iter() still returns &RowSelector (non-breaking); mask-backed selections lazily materialize a selector cache on first call, while internal hot paths bypass it and use the BooleanBuffer / MaskRunIter directly
  • row_count / skipped_row_count use a cached popcount
  • selects_any uses set_indices().next()
  • trim preserves mask backing via BooleanBuffer::slice
  • intersection / union on Mask+Mask use BitAnd / BitOr
  • split_off on a mask uses BooleanBuffer::slice (O(1), both halves stay mask-backed)
  • limit slices at the selected-row boundary via find_nth_set_bit_position, staying mask-backed
  • offset finds the first selected row to keep via find_nth_set_bit_position and rebuilds only the mask buffer, avoiding selector materialization
  • and_then applies the inner selection over the mask's set positions, returning a mask-backed result
  • FromIterator<RowSelection> concatenates BooleanBuffers when every input is mask-backed

Mixed inputs, and existing selector-backed inputs, still use the existing selector helpers. Existing callers keep the same behavior.

The reader (ReadPlanBuilder::build) passes a mask-backed selection straight to RowSelectionCursor::new_mask_from_buffer, so it skips rebuilding the bitmap from selectors.

Auto resolution works directly on the bitmap (early-exit run counting), without converting the backing.

Integrates with #10288: both mask cursor constructors carry LoadedRowRanges, so a caller-provided bitmap stays within loaded pages when page pruning skips pages.

Also adds MaskRunIter + RowSelection::as_mask for zero-allocation RLE iteration over a mask, the row_selector_boolean_buffer benchmark, and read_auto / mask-backed input modes in row_selection_cursor.

Are these changes tested?

Yes. This PR extends the existing RowSelection unit tests with coverage for:

  • constructing from BooleanBuffer, including empty and all-unset masks
  • From<BooleanBuffer>
  • preserving mask backing across clone, split_off, limit, offset, and_then, and all-mask FromIterator<RowSelection>
  • falling back to selector backing for mixed-backed concatenation
  • equality between equivalent selector-backed and mask-backed selections
  • mask-backed intersection / union, including uneven-length inputs
  • fuzz-style equivalence between mask-backed selections and the existing from_filters selector path
  • sparse-page regression tests from a non-byte-aligned BooleanBuffer::slice(...), under both Mask and Auto policies (fix(parquet): support mask filtering across skipped pages #10288 integration seam)
  • mask chunks stop at LoadedRowRanges boundaries
  • fuzz tests for boolean_mask_from_selectors and trim_mask (including non-zero offsets)
  • remaining branches checked with cargo llvm-cov

Are there any user-facing changes?

No breaking API changes. New public APIs: RowSelection::from_boolean_buffer, From<BooleanBuffer>, RowSelection::as_mask, MaskRunIter.

@github-actions github-actions Bot added the parquet Changes to the parquet crate label Jun 14, 2026
@alamb

alamb commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

@haohuaijin

Copy link
Copy Markdown
Contributor Author

Thanks @alamb for the pointers. I looked at #6624, #7454, and the work that landed in #8733.

My understanding is:

This does not replace selector-backed selections. Selectors are still better for clustered/page-index-style selections.

The case this helps is when the caller already has a row-level bitmap, such as from an external index, FTS index, or bitmap index. Today that bitmap has to go through BooleanBuffer -> Vec<RowSelector> -> mask in some paths. Keeping the original BooleanBuffer avoids that extra conversion and avoids building a large selector list for fragmented masks.

So I see this as a small representation-layer improvement that complements #8733. #8733 added mask execution; this PR adds a direct bitmap-backed input path for it. It does not try to implement the broader adaptive predicate-pushdown/page-cache design from #7454.

cc @hhhizzz @XiangpengHao @zhuqi-lucas

@alamb

alamb commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

run benchmark arrow_reader arrow_reader_row_filter

@adriangbot

Copy link
Copy Markdown

🤖 Arrow criterion benchmark running (GKE) | trigger
Instance: c4a-highmem-16 (12 vCPU / 65 GiB) | Linux bench-c4783293361-635-hxxkf 6.12.68+ #1 SMP Sat May 2 07:49:07 UTC 2026 aarch64 GNU/Linux

CPU Details (lscpu)
Architecture:                            aarch64
CPU op-mode(s):                          64-bit
Byte Order:                              Little Endian
CPU(s):                                  16
On-line CPU(s) list:                     0-15
Vendor ID:                               ARM
Model name:                              Neoverse-V2
Model:                                   1
Thread(s) per core:                      1
Core(s) per cluster:                     16
Socket(s):                               -
Cluster(s):                              1
Stepping:                                r0p1
BogoMIPS:                                2000.00
Flags:                                   fp asimd evtstrm aes pmull sha1 sha2 crc32 atomics fphp asimdhp cpuid asimdrdm jscvt fcma lrcpc dcpop sha3 sm3 sm4 asimddp sha512 sve asimdfhm dit uscat ilrcpc flagm sb paca pacg dcpodp sve2 sveaes svepmull svebitperm svesha3 svesm4 flagm2 frint svei8mm svebf16 i8mm bf16 dgh rng bti
L1d cache:                               1 MiB (16 instances)
L1i cache:                               1 MiB (16 instances)
L2 cache:                                32 MiB (16 instances)
L3 cache:                                80 MiB (1 instance)
NUMA node(s):                            1
NUMA node0 CPU(s):                       0-15
Vulnerability Gather data sampling:      Not affected
Vulnerability Indirect target selection: Not affected
Vulnerability Itlb multihit:             Not affected
Vulnerability L1tf:                      Not affected
Vulnerability Mds:                       Not affected
Vulnerability Meltdown:                  Not affected
Vulnerability Mmio stale data:           Not affected
Vulnerability Reg file data sampling:    Not affected
Vulnerability Retbleed:                  Not affected
Vulnerability Spec rstack overflow:      Not affected
Vulnerability Spec store bypass:         Mitigation; Speculative Store Bypass disabled via prctl
Vulnerability Spectre v1:                Mitigation; __user pointer sanitization
Vulnerability Spectre v2:                Mitigation; CSV2, BHB
Vulnerability Srbds:                     Not affected
Vulnerability Tsa:                       Not affected
Vulnerability Tsx async abort:           Not affected
Vulnerability Vmscape:                   Not affected

Comparing export-mask (1720537) to 2e035fd (merge-base) diff
BENCH_NAME=arrow_reader
BENCH_COMMAND=cargo bench --features=arrow,async,test_common,experimental,object_store --bench arrow_reader
BENCH_FILTER=
Results will be posted here when complete


File an issue against this benchmark runner

@adriangbot

Copy link
Copy Markdown

🤖 Arrow criterion benchmark running (GKE) | trigger
Instance: c4a-highmem-16 (12 vCPU / 65 GiB) | Linux bench-c4783293361-636-bs95k 6.12.68+ #1 SMP Sat May 2 07:49:07 UTC 2026 aarch64 GNU/Linux

CPU Details (lscpu)
Architecture:                            aarch64
CPU op-mode(s):                          64-bit
Byte Order:                              Little Endian
CPU(s):                                  16
On-line CPU(s) list:                     0-15
Vendor ID:                               ARM
Model name:                              Neoverse-V2
Model:                                   1
Thread(s) per core:                      1
Core(s) per cluster:                     16
Socket(s):                               -
Cluster(s):                              1
Stepping:                                r0p1
BogoMIPS:                                2000.00
Flags:                                   fp asimd evtstrm aes pmull sha1 sha2 crc32 atomics fphp asimdhp cpuid asimdrdm jscvt fcma lrcpc dcpop sha3 sm3 sm4 asimddp sha512 sve asimdfhm dit uscat ilrcpc flagm sb paca pacg dcpodp sve2 sveaes svepmull svebitperm svesha3 svesm4 flagm2 frint svei8mm svebf16 i8mm bf16 dgh rng bti
L1d cache:                               1 MiB (16 instances)
L1i cache:                               1 MiB (16 instances)
L2 cache:                                32 MiB (16 instances)
L3 cache:                                80 MiB (1 instance)
NUMA node(s):                            1
NUMA node0 CPU(s):                       0-15
Vulnerability Gather data sampling:      Not affected
Vulnerability Indirect target selection: Not affected
Vulnerability Itlb multihit:             Not affected
Vulnerability L1tf:                      Not affected
Vulnerability Mds:                       Not affected
Vulnerability Meltdown:                  Not affected
Vulnerability Mmio stale data:           Not affected
Vulnerability Reg file data sampling:    Not affected
Vulnerability Retbleed:                  Not affected
Vulnerability Spec rstack overflow:      Not affected
Vulnerability Spec store bypass:         Mitigation; Speculative Store Bypass disabled via prctl
Vulnerability Spectre v1:                Mitigation; __user pointer sanitization
Vulnerability Spectre v2:                Mitigation; CSV2, BHB
Vulnerability Srbds:                     Not affected
Vulnerability Tsa:                       Not affected
Vulnerability Tsx async abort:           Not affected
Vulnerability Vmscape:                   Not affected

Comparing export-mask (1720537) to 2e035fd (merge-base) diff
BENCH_NAME=arrow_reader_row_filter
BENCH_COMMAND=cargo bench --features=arrow,async,test_common,experimental,object_store --bench arrow_reader_row_filter
BENCH_FILTER=
Results will be posted here when complete


File an issue against this benchmark runner

@alamb alamb added performance api-change Changes to the arrow API labels Jun 23, 2026
@alamb

alamb commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

run benchmark arrow_reader_clickbench

@alamb

alamb commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

Merging up to fix CI

@adriangbot

Copy link
Copy Markdown

🤖 Arrow criterion benchmark running (GKE) | trigger
Instance: c4a-highmem-16 (12 vCPU / 65 GiB) | Linux bench-c4783400488-637-cprmq 6.12.68+ #1 SMP Sat May 2 07:49:07 UTC 2026 aarch64 GNU/Linux

CPU Details (lscpu)
Architecture:                            aarch64
CPU op-mode(s):                          64-bit
Byte Order:                              Little Endian
CPU(s):                                  16
On-line CPU(s) list:                     0-15
Vendor ID:                               ARM
Model name:                              Neoverse-V2
Model:                                   1
Thread(s) per core:                      1
Core(s) per cluster:                     16
Socket(s):                               -
Cluster(s):                              1
Stepping:                                r0p1
BogoMIPS:                                2000.00
Flags:                                   fp asimd evtstrm aes pmull sha1 sha2 crc32 atomics fphp asimdhp cpuid asimdrdm jscvt fcma lrcpc dcpop sha3 sm3 sm4 asimddp sha512 sve asimdfhm dit uscat ilrcpc flagm sb paca pacg dcpodp sve2 sveaes svepmull svebitperm svesha3 svesm4 flagm2 frint svei8mm svebf16 i8mm bf16 dgh rng bti
L1d cache:                               1 MiB (16 instances)
L1i cache:                               1 MiB (16 instances)
L2 cache:                                32 MiB (16 instances)
L3 cache:                                80 MiB (1 instance)
NUMA node(s):                            1
NUMA node0 CPU(s):                       0-15
Vulnerability Gather data sampling:      Not affected
Vulnerability Indirect target selection: Not affected
Vulnerability Itlb multihit:             Not affected
Vulnerability L1tf:                      Not affected
Vulnerability Mds:                       Not affected
Vulnerability Meltdown:                  Not affected
Vulnerability Mmio stale data:           Not affected
Vulnerability Reg file data sampling:    Not affected
Vulnerability Retbleed:                  Not affected
Vulnerability Spec rstack overflow:      Not affected
Vulnerability Spec store bypass:         Mitigation; Speculative Store Bypass disabled via prctl
Vulnerability Spectre v1:                Mitigation; __user pointer sanitization
Vulnerability Spectre v2:                Mitigation; CSV2, BHB
Vulnerability Srbds:                     Not affected
Vulnerability Tsa:                       Not affected
Vulnerability Tsx async abort:           Not affected
Vulnerability Vmscape:                   Not affected

Comparing export-mask (7b102f4) to 6ba533d (merge-base) diff
BENCH_NAME=arrow_reader_clickbench
BENCH_COMMAND=cargo bench --features=arrow,async,test_common,experimental,object_store --bench arrow_reader_clickbench
BENCH_FILTER=
Results will be posted here when complete


File an issue against this benchmark runner

@adriangbot

Copy link
Copy Markdown

🤖 Arrow criterion benchmark completed (GKE) | trigger

Instance: c4a-highmem-16 (12 vCPU / 65 GiB)

CPU Details (lscpu)
Architecture:                            aarch64
CPU op-mode(s):                          64-bit
Byte Order:                              Little Endian
CPU(s):                                  16
On-line CPU(s) list:                     0-15
Vendor ID:                               ARM
Model name:                              Neoverse-V2
Model:                                   1
Thread(s) per core:                      1
Core(s) per cluster:                     16
Socket(s):                               -
Cluster(s):                              1
Stepping:                                r0p1
BogoMIPS:                                2000.00
Flags:                                   fp asimd evtstrm aes pmull sha1 sha2 crc32 atomics fphp asimdhp cpuid asimdrdm jscvt fcma lrcpc dcpop sha3 sm3 sm4 asimddp sha512 sve asimdfhm dit uscat ilrcpc flagm sb paca pacg dcpodp sve2 sveaes svepmull svebitperm svesha3 svesm4 flagm2 frint svei8mm svebf16 i8mm bf16 dgh rng bti
L1d cache:                               1 MiB (16 instances)
L1i cache:                               1 MiB (16 instances)
L2 cache:                                32 MiB (16 instances)
L3 cache:                                80 MiB (1 instance)
NUMA node(s):                            1
NUMA node0 CPU(s):                       0-15
Vulnerability Gather data sampling:      Not affected
Vulnerability Indirect target selection: Not affected
Vulnerability Itlb multihit:             Not affected
Vulnerability L1tf:                      Not affected
Vulnerability Mds:                       Not affected
Vulnerability Meltdown:                  Not affected
Vulnerability Mmio stale data:           Not affected
Vulnerability Reg file data sampling:    Not affected
Vulnerability Retbleed:                  Not affected
Vulnerability Spec rstack overflow:      Not affected
Vulnerability Spec store bypass:         Mitigation; Speculative Store Bypass disabled via prctl
Vulnerability Spectre v1:                Mitigation; __user pointer sanitization
Vulnerability Spectre v2:                Mitigation; CSV2, BHB
Vulnerability Srbds:                     Not affected
Vulnerability Tsa:                       Not affected
Vulnerability Tsx async abort:           Not affected
Vulnerability Vmscape:                   Not affected
Details

group                                                                                              export-mask                            main
-----                                                                                              -----------                            ----
arrow_reader_row_filter/float64 <= 99.0/all_columns/async                                          1.02      5.4±0.03ms        ? ?/sec    1.00      5.3±0.03ms        ? ?/sec
arrow_reader_row_filter/float64 <= 99.0/all_columns/sync                                           1.01      5.2±0.04ms        ? ?/sec    1.00      5.2±0.06ms        ? ?/sec
arrow_reader_row_filter/float64 <= 99.0/exclude_filter_column/async                                1.01      4.6±0.04ms        ? ?/sec    1.00      4.6±0.03ms        ? ?/sec
arrow_reader_row_filter/float64 <= 99.0/exclude_filter_column/sync                                 1.00      4.5±0.04ms        ? ?/sec    1.00      4.5±0.05ms        ? ?/sec
arrow_reader_row_filter/float64 > 99.0 AND ts >= 9000/all_columns/async                            1.01      4.2±0.04ms        ? ?/sec    1.00      4.2±0.04ms        ? ?/sec
arrow_reader_row_filter/float64 > 99.0 AND ts >= 9000/all_columns/sync                             1.01      4.5±0.04ms        ? ?/sec    1.00      4.5±0.04ms        ? ?/sec
arrow_reader_row_filter/float64 > 99.0 AND ts >= 9000/exclude_filter_column/async                  1.02      3.7±0.03ms        ? ?/sec    1.00      3.7±0.01ms        ? ?/sec
arrow_reader_row_filter/float64 > 99.0 AND ts >= 9000/exclude_filter_column/sync                   1.01      3.7±0.04ms        ? ?/sec    1.00      3.7±0.03ms        ? ?/sec
arrow_reader_row_filter/float64 > 99.0/all_columns/async                                           1.01      5.4±0.03ms        ? ?/sec    1.00      5.3±0.03ms        ? ?/sec
arrow_reader_row_filter/float64 > 99.0/all_columns/sync                                            1.01      5.2±0.05ms        ? ?/sec    1.00      5.2±0.05ms        ? ?/sec
arrow_reader_row_filter/float64 > 99.0/exclude_filter_column/async                                 1.00      4.6±0.03ms        ? ?/sec    1.00      4.6±0.03ms        ? ?/sec
arrow_reader_row_filter/float64 > 99.0/exclude_filter_column/sync                                  1.00      4.5±0.03ms        ? ?/sec    1.00      4.5±0.04ms        ? ?/sec
arrow_reader_row_filter/int64 == 9999/all_columns/async                                            1.02    897.9±2.45µs        ? ?/sec    1.00    878.6±2.32µs        ? ?/sec
arrow_reader_row_filter/int64 == 9999/all_columns/sync                                             1.00      2.2±0.02ms        ? ?/sec    1.00      2.2±0.02ms        ? ?/sec
arrow_reader_row_filter/int64 == 9999/exclude_filter_column/async                                  1.03    822.5±2.61µs        ? ?/sec    1.00    798.2±2.02µs        ? ?/sec
arrow_reader_row_filter/int64 == 9999/exclude_filter_column/sync                                   1.00      2.1±0.02ms        ? ?/sec    1.00      2.1±0.02ms        ? ?/sec
arrow_reader_row_filter/int64 > 90/all_columns/async                                               1.09      8.3±0.12ms        ? ?/sec    1.00      7.6±0.05ms        ? ?/sec
arrow_reader_row_filter/int64 > 90/all_columns/sync                                                1.01      6.9±0.06ms        ? ?/sec    1.00      6.8±0.04ms        ? ?/sec
arrow_reader_row_filter/int64 > 90/exclude_filter_column/async                                     1.08      7.7±0.12ms        ? ?/sec    1.00      7.2±0.05ms        ? ?/sec
arrow_reader_row_filter/int64 > 90/exclude_filter_column/sync                                      1.01      6.4±0.05ms        ? ?/sec    1.00      6.3±0.04ms        ? ?/sec
arrow_reader_row_filter/ts < 9000/all_columns/async                                                1.01      5.0±0.04ms        ? ?/sec    1.00      5.0±0.04ms        ? ?/sec
arrow_reader_row_filter/ts < 9000/all_columns/sync                                                 1.00      5.3±0.04ms        ? ?/sec    1.00      5.3±0.04ms        ? ?/sec
arrow_reader_row_filter/ts < 9000/exclude_filter_column/async                                      1.00      4.5±0.04ms        ? ?/sec    1.01      4.6±0.03ms        ? ?/sec
arrow_reader_row_filter/ts < 9000/exclude_filter_column/sync                                       1.01      4.5±0.04ms        ? ?/sec    1.00      4.5±0.04ms        ? ?/sec
arrow_reader_row_filter/ts >= 9000/all_columns/async                                               1.00      3.4±0.03ms        ? ?/sec    1.00      3.4±0.03ms        ? ?/sec
arrow_reader_row_filter/ts >= 9000/all_columns/sync                                                1.00      3.6±0.03ms        ? ?/sec    1.01      3.6±0.02ms        ? ?/sec
arrow_reader_row_filter/ts >= 9000/exclude_filter_column/async                                     1.00      3.1±0.02ms        ? ?/sec    1.02      3.1±0.03ms        ? ?/sec
arrow_reader_row_filter/ts >= 9000/exclude_filter_column/sync                                      1.00      3.2±0.03ms        ? ?/sec    1.00      3.2±0.03ms        ? ?/sec
arrow_reader_row_filter/utf8View <> ''/all_columns/async                                           1.06      9.1±0.08ms        ? ?/sec    1.00      8.6±0.06ms        ? ?/sec
arrow_reader_row_filter/utf8View <> ''/all_columns/sync                                            1.01      9.4±0.06ms        ? ?/sec    1.00      9.3±0.06ms        ? ?/sec
arrow_reader_row_filter/utf8View <> ''/exclude_filter_column/async                                 1.06      7.6±0.07ms        ? ?/sec    1.00      7.2±0.04ms        ? ?/sec
arrow_reader_row_filter/utf8View <> ''/exclude_filter_column/sync                                  1.00      6.4±0.03ms        ? ?/sec    1.00      6.4±0.04ms        ? ?/sec
arrow_reader_row_filter_limit/float64 <= 99.0/all_columns/limit10/async                            1.03    347.8±1.64µs        ? ?/sec    1.00    336.2±0.81µs        ? ?/sec
arrow_reader_row_filter_limit/float64 <= 99.0/exclude_filter_column/limit10/async                  1.05    344.0±1.71µs        ? ?/sec    1.00    326.9±0.57µs        ? ?/sec
arrow_reader_row_filter_limit/float64 > 99.0 AND ts >= 9000/all_columns/limit10/async              1.03    384.5±1.68µs        ? ?/sec    1.00    374.1±1.44µs        ? ?/sec
arrow_reader_row_filter_limit/float64 > 99.0 AND ts >= 9000/exclude_filter_column/limit10/async    1.04    371.2±2.32µs        ? ?/sec    1.00    356.9±1.02µs        ? ?/sec
arrow_reader_row_filter_limit/float64 > 99.0/all_columns/limit10/async                             1.03    348.1±1.56µs        ? ?/sec    1.00    337.2±1.29µs        ? ?/sec
arrow_reader_row_filter_limit/float64 > 99.0/exclude_filter_column/limit10/async                   1.05    345.4±1.73µs        ? ?/sec    1.00    327.9±0.97µs        ? ?/sec
arrow_reader_row_filter_limit/int64 > 90/all_columns/limit10/async                                 1.03    341.7±2.09µs        ? ?/sec    1.00    330.5±1.30µs        ? ?/sec
arrow_reader_row_filter_limit/int64 > 90/exclude_filter_column/limit10/async                       1.04    336.2±1.81µs        ? ?/sec    1.00    324.6±1.56µs        ? ?/sec
arrow_reader_row_filter_limit/ts < 9000/all_columns/limit10/async                                  1.04    338.6±1.61µs        ? ?/sec    1.00    326.5±1.17µs        ? ?/sec
arrow_reader_row_filter_limit/ts < 9000/exclude_filter_column/limit10/async                        1.05    335.8±1.76µs        ? ?/sec    1.00    321.1±1.14µs        ? ?/sec
arrow_reader_row_filter_limit/ts >= 9000/all_columns/limit10/async                                 1.03    354.8±1.67µs        ? ?/sec    1.00    344.6±1.16µs        ? ?/sec
arrow_reader_row_filter_limit/ts >= 9000/exclude_filter_column/limit10/async                       1.05    349.7±1.31µs        ? ?/sec    1.00    334.1±0.97µs        ? ?/sec
arrow_reader_row_filter_limit/utf8View <> ''/all_columns/limit10/async                             1.06    373.9±1.52µs        ? ?/sec    1.00    353.8±0.99µs        ? ?/sec
arrow_reader_row_filter_limit/utf8View <> ''/exclude_filter_column/limit10/async                   1.03    353.8±1.27µs        ? ?/sec    1.00    343.5±0.93µs        ? ?/sec

Resource Usage

base (merge-base)

Metric Value
Wall time 480.1s
Peak memory 45.0 MiB
Avg memory 30.9 MiB
CPU user 473.1s
CPU sys 0.1s
Peak spill 0 B

branch

Metric Value
Wall time 470.1s
Peak memory 44.8 MiB
Avg memory 31.1 MiB
CPU user 467.5s
CPU sys 0.0s
Peak spill 0 B

File an issue against this benchmark runner

@alamb

alamb commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

run benchmark row_selector row_selection_cursor

@alamb alamb left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you @haohuaijin . I think this is totally the right direction and I really like where this is going

As long as this doesn't cause a performance regression I think it is good to go.

I have actually tried this myself (see #6624) as did @XiangpengHao in #6624.

Some other thoughts

  1. We need to make sure it doesn't cause a performance regression (I can help here)
  2. It currently has a public API change, so we can't merge it until main opens for the next major release (likely in mid-late July). If we can figure out how to keep the API the same we could potentially release it sooner

I haven' tmade it through the entire PR yet, but I did fire off some benchmarks


#[inline]
fn next(&mut self) -> Option<RowSelector> {
match self {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In general I think this will add a branch in the main loops - every chunk of bits will require matching -- it might make sense to figure out how to have iterators for the two different types, so that code paths that want to iterate over the selection can match outside

match iter { 
  RowSelectionIter::Selectors(iter) => {  for i in iter ... }, // special loop for selector backed
  RowSelectionIter::Mask(iter) => {  for i in iter ... }, // special loop for mask backed
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated in fff3ab3

if self.finished {
return None;
}
match self.slices.next() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Much of this code appears untested

I ran code coverage like

  cargo llvm-cov --html test -p parquet --features arrow
Image

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added in fff3ab3

impl Iterator for MaskRunIter<'_> {
type Item = RowSelector;

fn next(&mut self) -> Option<RowSelector> {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is the main API that is not compatible -- it need to return &RowSelector -- if we could change that we can merge this PR before the next major release (with breaking API changes)

@haohuaijin haohuaijin Jun 24, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I updated RowSelection::iter() to keep returning Iterator<Item = &RowSelector>.

  • For selector-backed selections this still borrows from the internal Vec<RowSelector>.
  • For mask-backed selections I added a lazy selector cache, because the returned &RowSelector values need stable storage inside RowSelection; otherwise they would point into a temporary Vec.

The internal hot paths still avoid this cache and use the BooleanBuffer or MaskRunIter directly.

the struct is below

pub struct RowSelection {
    inner: RowSelectionInner,
}

pub(crate) enum RowSelectionInner {
    Selectors(Vec<RowSelector>),
    Mask(Box<MaskSelection>),
}

pub(crate) struct MaskSelection {
    mask: BooleanBuffer,
    selectors: OnceLock<Vec<RowSelector>>,
}

I am not fully sure this is the best approach, so suggestions and feedback are welcome.

if let (Some(l), Some(r)) = (self.as_mask(), other.as_mask()) {
return Self::from_boolean_buffer(intersect_masks(l, r));
}
let l = self.materialize_for_combine();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is a clever idea -- as a follow on we could potentially implement specialized versions of each of these implementations

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for example, I think and_then can use some of the fancy BMI instructions that @devanbenz is investigating in #10136

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agree

// Verify that the size of RowGroupDecoderState does not grow too large
fn test_structure_size() {
assert_eq!(std::mem::size_of::<RowGroupDecoderState>(), 232);
assert_eq!(std::mem::size_of::<RowGroupDecoderState>(), 256);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is bigger because a RowSelection is now bigger.

@adriangbot

Copy link
Copy Markdown

🤖 Arrow criterion benchmark running (GKE) | trigger
Instance: c4a-highmem-16 (12 vCPU / 65 GiB) | Linux bench-c4783482356-640-s7mgp 6.12.68+ #1 SMP Sat May 2 07:49:07 UTC 2026 aarch64 GNU/Linux

CPU Details (lscpu)
Architecture:                            aarch64
CPU op-mode(s):                          64-bit
Byte Order:                              Little Endian
CPU(s):                                  16
On-line CPU(s) list:                     0-15
Vendor ID:                               ARM
Model name:                              Neoverse-V2
Model:                                   1
Thread(s) per core:                      1
Core(s) per cluster:                     16
Socket(s):                               -
Cluster(s):                              1
Stepping:                                r0p1
BogoMIPS:                                2000.00
Flags:                                   fp asimd evtstrm aes pmull sha1 sha2 crc32 atomics fphp asimdhp cpuid asimdrdm jscvt fcma lrcpc dcpop sha3 sm3 sm4 asimddp sha512 sve asimdfhm dit uscat ilrcpc flagm sb paca pacg dcpodp sve2 sveaes svepmull svebitperm svesha3 svesm4 flagm2 frint svei8mm svebf16 i8mm bf16 dgh rng bti
L1d cache:                               1 MiB (16 instances)
L1i cache:                               1 MiB (16 instances)
L2 cache:                                32 MiB (16 instances)
L3 cache:                                80 MiB (1 instance)
NUMA node(s):                            1
NUMA node0 CPU(s):                       0-15
Vulnerability Gather data sampling:      Not affected
Vulnerability Indirect target selection: Not affected
Vulnerability Itlb multihit:             Not affected
Vulnerability L1tf:                      Not affected
Vulnerability Mds:                       Not affected
Vulnerability Meltdown:                  Not affected
Vulnerability Mmio stale data:           Not affected
Vulnerability Reg file data sampling:    Not affected
Vulnerability Retbleed:                  Not affected
Vulnerability Spec rstack overflow:      Not affected
Vulnerability Spec store bypass:         Mitigation; Speculative Store Bypass disabled via prctl
Vulnerability Spectre v1:                Mitigation; __user pointer sanitization
Vulnerability Spectre v2:                Mitigation; CSV2, BHB
Vulnerability Srbds:                     Not affected
Vulnerability Tsa:                       Not affected
Vulnerability Tsx async abort:           Not affected
Vulnerability Vmscape:                   Not affected

Comparing export-mask (7b102f4) to 6ba533d (merge-base) diff
BENCH_NAME=row_selection_cursor
BENCH_COMMAND=cargo bench --features=arrow,async,test_common,experimental,object_store --bench row_selection_cursor
BENCH_FILTER=
Results will be posted here when complete


File an issue against this benchmark runner

@adriangbot

Copy link
Copy Markdown

🤖 Arrow criterion benchmark running (GKE) | trigger
Instance: c4a-highmem-16 (12 vCPU / 65 GiB) | Linux bench-c4783482356-639-b2k9m 6.12.68+ #1 SMP Sat May 2 07:49:07 UTC 2026 aarch64 GNU/Linux

CPU Details (lscpu)
Architecture:                            aarch64
CPU op-mode(s):                          64-bit
Byte Order:                              Little Endian
CPU(s):                                  16
On-line CPU(s) list:                     0-15
Vendor ID:                               ARM
Model name:                              Neoverse-V2
Model:                                   1
Thread(s) per core:                      1
Core(s) per cluster:                     16
Socket(s):                               -
Cluster(s):                              1
Stepping:                                r0p1
BogoMIPS:                                2000.00
Flags:                                   fp asimd evtstrm aes pmull sha1 sha2 crc32 atomics fphp asimdhp cpuid asimdrdm jscvt fcma lrcpc dcpop sha3 sm3 sm4 asimddp sha512 sve asimdfhm dit uscat ilrcpc flagm sb paca pacg dcpodp sve2 sveaes svepmull svebitperm svesha3 svesm4 flagm2 frint svei8mm svebf16 i8mm bf16 dgh rng bti
L1d cache:                               1 MiB (16 instances)
L1i cache:                               1 MiB (16 instances)
L2 cache:                                32 MiB (16 instances)
L3 cache:                                80 MiB (1 instance)
NUMA node(s):                            1
NUMA node0 CPU(s):                       0-15
Vulnerability Gather data sampling:      Not affected
Vulnerability Indirect target selection: Not affected
Vulnerability Itlb multihit:             Not affected
Vulnerability L1tf:                      Not affected
Vulnerability Mds:                       Not affected
Vulnerability Meltdown:                  Not affected
Vulnerability Mmio stale data:           Not affected
Vulnerability Reg file data sampling:    Not affected
Vulnerability Retbleed:                  Not affected
Vulnerability Spec rstack overflow:      Not affected
Vulnerability Spec store bypass:         Mitigation; Speculative Store Bypass disabled via prctl
Vulnerability Spectre v1:                Mitigation; __user pointer sanitization
Vulnerability Spectre v2:                Mitigation; CSV2, BHB
Vulnerability Srbds:                     Not affected
Vulnerability Tsa:                       Not affected
Vulnerability Tsx async abort:           Not affected
Vulnerability Vmscape:                   Not affected

Comparing export-mask (7b102f4) to 6ba533d (merge-base) diff
BENCH_NAME=row_selector
BENCH_COMMAND=cargo bench --features=arrow,async,test_common,experimental,object_store --bench row_selector
BENCH_FILTER=
Results will be posted here when complete


File an issue against this benchmark runner

@adriangbot

Copy link
Copy Markdown

🤖 Arrow criterion benchmark completed (GKE) | trigger

Instance: c4a-highmem-16 (12 vCPU / 65 GiB)

CPU Details (lscpu)
Architecture:                            aarch64
CPU op-mode(s):                          64-bit
Byte Order:                              Little Endian
CPU(s):                                  16
On-line CPU(s) list:                     0-15
Vendor ID:                               ARM
Model name:                              Neoverse-V2
Model:                                   1
Thread(s) per core:                      1
Core(s) per cluster:                     16
Socket(s):                               -
Cluster(s):                              1
Stepping:                                r0p1
BogoMIPS:                                2000.00
Flags:                                   fp asimd evtstrm aes pmull sha1 sha2 crc32 atomics fphp asimdhp cpuid asimdrdm jscvt fcma lrcpc dcpop sha3 sm3 sm4 asimddp sha512 sve asimdfhm dit uscat ilrcpc flagm sb paca pacg dcpodp sve2 sveaes svepmull svebitperm svesha3 svesm4 flagm2 frint svei8mm svebf16 i8mm bf16 dgh rng bti
L1d cache:                               1 MiB (16 instances)
L1i cache:                               1 MiB (16 instances)
L2 cache:                                32 MiB (16 instances)
L3 cache:                                80 MiB (1 instance)
NUMA node(s):                            1
NUMA node0 CPU(s):                       0-15
Vulnerability Gather data sampling:      Not affected
Vulnerability Indirect target selection: Not affected
Vulnerability Itlb multihit:             Not affected
Vulnerability L1tf:                      Not affected
Vulnerability Mds:                       Not affected
Vulnerability Meltdown:                  Not affected
Vulnerability Mmio stale data:           Not affected
Vulnerability Reg file data sampling:    Not affected
Vulnerability Retbleed:                  Not affected
Vulnerability Spec rstack overflow:      Not affected
Vulnerability Spec store bypass:         Mitigation; Speculative Store Bypass disabled via prctl
Vulnerability Spectre v1:                Mitigation; __user pointer sanitization
Vulnerability Spectre v2:                Mitigation; CSV2, BHB
Vulnerability Srbds:                     Not affected
Vulnerability Tsa:                       Not affected
Vulnerability Tsx async abort:           Not affected
Vulnerability Vmscape:                   Not affected
Details

group           export-mask                            main
-----           -----------                            ----
and_then        1.48   862.5±15.70µs        ? ?/sec    1.00    581.9±2.03µs        ? ?/sec
from_filters    1.00    370.2±1.96µs        ? ?/sec    1.01    372.2±5.17µs        ? ?/sec
intersection    2.03      2.7±0.02ms        ? ?/sec    1.00   1338.6±2.46µs        ? ?/sec
union           2.38      3.1±0.03ms        ? ?/sec    1.00   1294.3±3.77µs        ? ?/sec

Resource Usage

base (merge-base)

Metric Value
Wall time 50.0s
Peak memory 14.0 MiB
Avg memory 10.4 MiB
CPU user 44.6s
CPU sys 0.0s
Peak spill 0 B

branch

Metric Value
Wall time 45.0s
Peak memory 13.5 MiB
Avg memory 10.8 MiB
CPU user 30.9s
CPU sys 9.1s
Peak spill 0 B

File an issue against this benchmark runner

@alamb

alamb commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

🤔 looks like arrow_reader_row_filter is slightly slower. Rerunning to try an reproduce

@alamb

alamb commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

run benchmark arrow_reader_row_filter

@haohuaijin

Copy link
Copy Markdown
Contributor Author

Thanks for your reviews @hhhizzz

@alamb alamb left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you @haohuaijin and @hhhizzz -- I found this PR very easy to read and understand; It is also super well tested, and I think is a foundational piece for more efficient filter evaluation in parquet

cc @etseidl for your interest in all things parquet
cc @devanbenz as maybe some of your BMI work is relevant / could be used here
cc @Dandandan as we have discussed this before in the context of pushdown filter work (when the manipulation of RowSelections was expensive)
cc @adriangb as I think you are also interested in this

Comment thread parquet/src/arrow/arrow_reader/selection/mod.rs
Comment thread parquet/src/arrow/arrow_reader/selection/mod.rs
Comment thread parquet/src/arrow/arrow_reader/selection/mod.rs
Comment thread parquet/src/arrow/arrow_reader/selection/mod.rs Outdated
Comment thread parquet/src/arrow/arrow_reader/selection/mod.rs
Comment thread parquet/src/arrow/arrow_reader/selection/mod.rs Outdated
Comment thread parquet/src/arrow/arrow_reader/read_plan.rs
if l.len() == r.len() {
return l | r;
}
let common = l.len().min(r.len());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you might be able to do this more efficiently by copying the longer one and then using &= -- maybe in some follow on

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

track in #10425

Comment thread parquet/src/arrow/arrow_reader/mod.rs
@haohuaijin

haohuaijin commented Jul 24, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for your review @alamb ❤️, i apply the suggestion, and filed follow-up issues to track some performance improve. and i also love to pick up and finish below performance improve.

The RowSelectionIter slice-iterator specializations (review finding 5) are addressed in this PR directly in 86610b5 and bdf2726

cc @hhhizzz

@alamb

alamb commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Let;s go go go go!

@alamb
alamb merged commit f0ec758 into apache:main Jul 24, 2026
17 checks passed
@haohuaijin
haohuaijin deleted the export-mask branch July 25, 2026 02:35
@haohuaijin

Copy link
Copy Markdown
Contributor Author

Thanks again for the reviews and suggestions @hhhizzz @alamb

alamb pushed a commit that referenced this pull request Jul 25, 2026
#10434)

# Which issue does this PR close?

- Closes #10424.

# Rationale for this change

Follow up to #10141. `selection/mod.rs` had grown past 2200 lines and
mixed the `RowSelection` type and its public API, the set algebra, the
page range mapping, the cursor machinery and a large test module. This
is a reorganization to make the code easier to navigate; there is no
functional change.

# What changes are included in this PR?

The module is split by concern, and `mod.rs` goes from 2219 to 947
lines:

| module | contents |
| --- | --- |
| `mod.rs` | `RowSelection`, `RowSelectionInner` and the public API |
| `selector.rs` | run length backing: `RowSelector`, `RowSelectionIter`,
and the `split_off` / `offset` / `limit` primitives |
| `boolean.rs` | bitmap backing: `MaskSelection`, `MaskRunIter` and the
mask primitives |
| `algebra.rs` | `and_then`, `intersection` and `union` for both
backings |
| `ranges.rs` | page byte ranges and batch boundary expansion |
| `cursor.rs` | `RowSelectionCursor`, `MaskCursor`, `SelectorsCursor`,
`LoadedRowRanges`, `RowSelectionPolicy` / `RowSelectionStrategy` |

# Are these changes tested?

Yes, by the existing tests, moved next to the code they now cover. No
test was added, removed or renamed: the set of test names under
`arrow::arrow_reader::selection` is identical before and after this PR,
and the `split_off` / `trim` / `offset` / `limit` tests still go through
the `RowSelection` methods rather than the extracted helpers.

# Are there any user-facing changes?

No. The public API (`parquet::arrow::arrow_reader::{RowSelection,
RowSelector, RowSelectionCursor, RowSelectionPolicy, RowSelectionIter,
MaskRunIter}`) is unchanged, and no call site outside `selection/`
needed modification.

One internal note: `SelectorsCursor` and `MaskChunk` are no longer
nameable outside `selection/`, as they moved into the private `cursor`
module and are not re-exported. Neither was reachable from outside the
crate before, since `selection` is `pub(crate)`.
@alamb

alamb commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

I think we are very close to being able to implement something like this

We probably already do it for unnested (leaf) nodes -- but need a bit more cleverness for structured nodes

@Jefffrey

Copy link
Copy Markdown
Contributor

was this PR still considered an API change? i took a brief look and noticed:

  • RowSelection no longer implements Debug, Eq and PartialEq
  • RowSelection::iter return changed from impl Iterator<Item = &RowSelector> to RowSelectionIter<'_>

i think both are these are considered breaking changes

@hhhizzz

hhhizzz commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

was this PR still considered an API change? i took a brief look and noticed:

  • RowSelection no longer implements Debug, Eq and PartialEq
  • RowSelection::iter return changed from impl Iterator<Item = &RowSelector> to RowSelectionIter<'_>

i think both are these are considered breaking changes

Hi,@Jefffrey ,RowSelection still implements Debug, PartialEq, and Eq; they were changed from derives to manual implementations. The iter() signature did change syntactically, but exposing a concrete type that preserves the previously promised Iterator<Item = &RowSelector> contract is source-compatible for existing callers. It does create a new concrete-type commitment going forward, though.

@Jefffrey

Copy link
Copy Markdown
Contributor

they were changed from derives to manual implementations

thanks i missed that

The iter() signature did change syntactically, but exposing a concrete type that preserves the previously promised Iterator<Item = &RowSelector> contract is source-compatible for existing callers. It does create a new concrete-type commitment going forward, though.

thoughts on if we should revert to the old signature (if possible) just to be on the safe side? i think only really niche use cases would be broken but maybe its good to keep flexibility if ever we need it 🤔

@hhhizzz

hhhizzz commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Yes, I agree that restoring the opaque return type would preserve the original abstraction boundary and give us more flexibility in the future.

@haohuaijin, would you mind preparing a follow-up PR to restore the old iter() signature and make RowSelectionIter internal again?

cc @alamb for thoughts.

@haohuaijin

Copy link
Copy Markdown
Contributor Author

@haohuaijin, would you mind preparing a follow-up PR to restore the old iter() signature and make RowSelectionIter internal again?

sure, it is okay to restore the old iter() signature, i will make a pr later

@haohuaijin

Copy link
Copy Markdown
Contributor Author

#10450 pr is ready

@Jefffrey Jefffrey removed the api-change Changes to the arrow API label Jul 28, 2026
Jefffrey pushed a commit that referenced this pull request Jul 28, 2026
…0450)

# Which issue does this PR close?

- Follow-on to #10141, addressing
#10141 (comment)

# Rationale for this change

#10141 changed `RowSelection::iter` from `impl Iterator<Item =
&RowSelector>` to a named type, `RowSelectionIter<'_>`. As @Jefffrey
pointed out, the opaque return type is worth keeping so we stay free to
change the implementation later.

# What changes are included in this PR?

`RowSelection::iter` returns `impl Iterator<Item = &RowSelector>` again.
Both match arms are already `std::slice::Iter<'_, RowSelector>`, so
`RowSelectionIter` is not needed at all and is removed rather than made
private. No behaviour change.

# Are these changes tested?

Covered by the existing `RowSelection` tests, which call `iter()` on
both backings. `cargo test -p parquet --all-features` passes.

# Are there any user-facing changes?

No. `RowSelectionIter` was added in #10141 and never released, and
`iter()` keeps the `Iterator<Item = &RowSelector>` contract callers
already relied on.
@alamb

alamb commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Thanks for the catch @Jefffrey @haohuaijin and @hhhizzz

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

parquet Changes to the parquet crate performance

Projects

None yet

6 participants