Repository navigation
perf(scan): merge narrow splits at boundaries only some fields of a struct have - #11128
robert3005 merged 10 commits into
Conversation
…truct have A scan splits wherever any column's chunk ends, and a split decodes the whole chunk of every column it overlaps. Where a struct's fields chunk at unrelated rows, as list elements and dictionary-encoded columns of a wide nested file do, the union holds spans of a few dozen rows, and successive splits each decode the same chunks of the other fields. The struct reader now marks the boundaries only some of its fields have as soft. SplitBy::LayoutSubSplitting merges spans ending at soft boundaries up to DEFAULT_MIN_SPLIT_ROWS (8,192, or max_rows if smaller). Boundaries every field has, its validity boundaries, and those layouts above the struct register, such as top-level chunk ends, are never merged, so files whose columns chunk alike split as before. Co-Authored-By: Claude Opus 5.5 <[email protected]> Signed-off-by: Christopher Wirt <[email protected]>
Merging this PR will not alter performance2 warnings
Performance Changes
Comparing Footnotes
|
|
The CodSpeed regression is from soft-boundary classification running under I'll also add a |
SplitBy::Layout never reads soft marks but paid for per-field collection, intersection and classification in StructReader. RowSplits now carries a BoundaryMode; Layout requests PositionsOnly and takes the original path. Co-Authored-By: Claude Sonnet 5.5 <[email protected]> Signed-off-by: Christopher Wirt <[email protected]>
|
@ChristopherWirt is attempting to deploy a commit to the Vortex Team on Vercel. A member of the Team first needs to authorize it. |
…x_rows StructReader's WithSoftMarks path now skips fields whose boundaries match the previous field's (the common aligned-columns case), intersects with a linear merge instead of binary searches, and collects straight into the parent RowSplits, removing RowSplits::append and remove_soft. Soft marks are deduplicated before they are stored, and fields with no boundaries in the range no longer empty the shared set. merge_small_spans keeps a soft boundary where dropping it would leave a span wider than max_rows, which subdivide_large_spans would otherwise re-split at a row no column chunks at. Also fixes the struct reader tests, which collected in PositionsOnly mode after the previous commit and so saw no soft marks; they now reuse register_boundaries and return VortexResult. Co-Authored-By: Claude Opus 5.5 <[email protected]> Signed-off-by: Christopher Wirt <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]> Signed-off-by: Christopher Wirt <[email protected]>
Validity boundaries were kept hard only at their own struct; a parent struct still marked them soft when its other fields lacked them, so whether they merged depended on nesting depth. Validity now takes part in the shared-boundary intersection as another child, which is consistent at every depth and removes the special case. Co-Authored-By: Claude Opus 5.5 <[email protected]> Signed-off-by: Christopher Wirt <[email protected]>
|
Follow-up commits:
Locally (divan), |
Signed-off-by: Christopher Wirt <[email protected]>
robert3005
left a comment
There was a problem hiding this comment.
Thanks for the contribution! I think this can be slightly simplified.
Signed-off-by: Christopher Wirt <[email protected]>
|
@robert3005 CodSpeed now reports |
|
If you could trim it down a bit further that would be great |
Signed-off-by: Christopher Wirt <[email protected]>
Summary
Problem: A scan splits a file wherever any column's chunk ends, and a split decodes the whole chunk of every column it overlaps. When a struct's fields chunk at unrelated rows, as list elements and dictionary-encoded columns of a wide nested file do, the union of their boundaries leaves spans of a few dozen rows. Successive splits then each decode the same chunks of the other fields. On a 180 MB list-layout file, a full scan held 3.6 GiB of native memory.
Fix: The struct reader marks boundaries that only some of its fields have as soft.
SplitBy::LayoutSubSplittingmerges spans that end at a soft boundary until they reach 8,192 rows. Boundaries that every field has are never merged, and neither are boundaries from layouts above the struct, so files whose columns chunk alike split as before.Result: full scan of one file, with Arrow conversion as JNI does it. Live native bytes come from a counting allocator:
Changes
RowSplitsgains soft marks. Each struct field registers into its ownRowSplits, and the struct takes their union. Any boundary that not every field has is marked soft.SplitBy::Layoutcollects positions only (BoundaryMode::PositionsOnly), so it skips the classification.SplitBy::LayoutSubSplittingmerges spans ending at a soft boundary until they reachDEFAULT_MIN_SPLIT_ROWS(8,192, the writer's row block), ormax_rowsif that is smaller. It never merges a span pastmax_rows. The merge runs before the existing sub-division of wide spans.Tests
RepartitionStrategy:LayoutSubSplittingmerges soft boundaries and keeps shared boundaries, for nullable and non-nullable structs.merge_small_spans: unit cases plus 1,000 generated cases. They check endpoints, hard boundaries, exact coverage, minimum span length at kept soft boundaries, and the sub-division that follows.*_sub_splittingvariants that measure the defaultSplitBy.On the fork (quantcast#67, same change), these passed: 836
vortex-layoutlibrary tests; Clippy forvortex-layoutandvortex-file(all targets, all features,-D warnings); pinned rustfmt; the JNI and Spark builds;git diff --check.This branch cherry-picks that change onto
developwithout conflicts. On the port, with the follow-up commits: 750vortex-layouttests; Clippy forvortex-layoutandvortex-file(all targets, all features,-D warnings); pinned rustfmt;git diff --check.Update on 2026-11-09
Merged upstream
develop(1c60cac79) cleanly inb7ebcd336, including the existing ty version pin and renamed-rule fix that address the Python lint failure.Compared
missing_stats[16]on the updated PR against the same upstream commit, using the repository's bench profile and locked dependencies on this macOS ARM64 host. Both builds passed. Six runs per version were interleaved in base/PR/PR/base order, each with 10,000 samples of 100 calls. Median of the per-run medians:developThe local difference is 1.35%, with overlapping per-run median ranges (upstream 765.5–791.7 ns; PR 772.6–792.2 ns). This does not reproduce the earlier 17.96% CodSpeed simulation regression. The pruning implementation, benchmark fixture and lockfile are identical to the current upstream baseline; the PR changes split collection. Native walltime is supporting evidence and does not replace the CI CPU simulation.
Reproduction after building each checkout with
cargo bench -p vortex-layout --bench zone_map_prune --no-run --locked:Fresh CI and CodSpeed workflows are awaiting maintainer approval. No additional tests, lint or formatting checks were run for this merge.
Not done here
AI assistance
Disclosed per the AI policy: AI tools helped write and review this change. Claude Code prepared this upstream port, issue and PR description at my request. I am the responsible contributor.
🤖 Generated with Claude Code