perf(parquet): reuse MaskSelection's cached selectors when converting to selectors - #10443
perf(parquet): reuse MaskSelection's cached selectors when converting to selectors#10443haohuaijin wants to merge 3 commits into
MaskSelection's cached selectors when converting to selectors#10443Conversation
…ng to selectors `RowSelection::iter()` lazily materializes and caches the RLE form of a mask-backed selection, but every path that converted such a selection into `RowSelector`s ignored that cache and re-encoded the bitmap. Add `MaskSelection::into_selectors`, which takes the `OnceLock` value when it was populated and otherwise falls back to `mask_to_selectors`, and use it from `RowSelection::into_selectors_vec` (backing `From<RowSelection>` for both `Vec<RowSelector>` and `VecDeque<RowSelector>`) and from `build_cursor` when lowering a mask-backed selection to the selector cursor. `intersection`/`union` reuse the cache too, via `borrowed_selectors`, which borrows it when populated and otherwise converts into a temporary rather than populating the cache of a selection the caller still owns. `mask_to_selectors` no longer has callers outside the `selection` module, so drop its `pub(crate)` re-export. Benchmarked with the new `mask_iterate_then_consume` /`mask_consume` cases in `row_selector`, which cover a caller that iterates a selection before consuming it, as `ParquetAccessPlan::into_overall_row_selection` does. Over 300k rows the iterate-then-consume conversion drops 46-56% across run lengths, and the cold-cache conversion is unchanged.
|
Thanks for working on this optimization. I independently reproduced the substantial warm-cache improvement, and the cache-reuse approach looks effective for the intended I did, however, find a clear and reproducible regression in the cold-cache The absolute difference is small—about 1.6 µs per conversion of a 300k-row mask—so this may not be a serious real-world regression, and it does not invalidate the main improvement. However, the result is stable and contradicts the PR's statement that all cold-cache cases remain within 1.5% of Could you please try reproducing this case in your environment? DetailsReproduction setup
First, I ran the complete cargo bench -p parquet --bench row_selector -- mask_Criterion settings: The cold-cache geometric means were:
I then isolated cargo bench -p parquet --bench row_selector -- 'mask_consume/run64'Focused Criterion settings: Focused result:
The values were stable across all five repetitions. As a small diagnostic, adding The main warm-cache improvement was reproduced and remains substantial. My concern is specifically the unexpected cold |
Thanks for checking this on Mac. I profiled the regression on an AMD EPYC 9254 (Zen 4) and reran every input as a separately compiled single-case benchmark. With three alternating rounds:
The original run128 regression appears to be an x86_64 code-layout/alignment artifact. So I don’t think the previously reported cold-path regression is caused by this PR or should block it. The cold microbenchmark is simply quite sensitive to binary layout on this Zen 4 machine. |
hhhizzz
left a comment
There was a problem hiding this comment.
Looks good to me. Thank you!

Which issue does this PR close?
MaskSelection's cached selectors when converting a mask-backedRowSelectionto selectors #10422.Rationale for this change
Follow up to #10141.
RowSelection::iter()caches the RLE form of a mask-backed selection, but the conversions toRowSelectors 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_selectorstakes the cachedVecwhen it was populated and otherwise falls back tomask_to_selectors. Used byRowSelection::into_selectors_vec(which backsFrom<RowSelection>forVec<RowSelector>andVecDeque<RowSelector>) and bybuild_cursor.MaskSelection::borrowed_selectorsdoes the same forintersectionandunion, returningCow::Borrowedwhen 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_selectorshas no callers outside theselectionmodule now, so its re-export is dropped.Are these changes tested?
Four new unit tests in
arrow_reader::selection::booleancover the cache being moved out rather than re-encoded (asserted on theVec's pointer), the cold-cache fallback,borrowed_selectorsin both states, andintersection/unionagreeing either way.cargo test -p parquet --all-featurespasses.row_selectorgains two scenarios over a mask-backed selection of 300k rows:mask_iterate_then_consumeiterates and then converts, asParquetAccessPlan::into_overall_row_selectiondoes, andmask_consumeconverts with a cold cache.mask_iterate_then_consume/run01mask_iterate_then_consume/run04mask_iterate_then_consume/run16mask_iterate_then_consume/run64mask_iterate_then_consume/randommask_consumeis unchanged: its medians over three runs per side land within 1.5% ofmain, inside the run-to-run spreadmainshows on its own.Are there any user-facing changes?
No, the added methods are internal.