perf(array): stop flattening appended list views in ListViewBuilder [builders-child-stack] - #9046
Conversation
Merging this PR will regress 1 benchmark
Warning Please fix the performance issues or acknowledge them on CodSpeed. Performance Changes
Tip Investigate this regression by commenting Comparing Footnotes
|
bdb1acd to
5343548
Compare
5343548 to
0779ba5
Compare
`append_listview_array` rebuilt every incoming `ListViewArray` into an exact layout before appending it. Rebasing offsets by the number of elements already in the builder is correct whatever layout they have, so the rebuild bought nothing except an unconditional promise that the finished array is zero-copyable to a `ListArray` - and it cost the caller any sharing the source expressed. A constant list array is the case that matters: canonicalizing one already points every view at a single copy of the value, and flattening it materialized one copy per row. Appending a 10,000-row constant list of three elements produced 30,000 elements; it now produces 3. Keep trimming unreferenced elements, but otherwise append the views as they arrived and track whether the result is still zero-copyable to a `ListArray` instead of asserting it. The flag is per-array and consumers already branch on it, so callers that need an exact layout can rebuild. Signed-off-by: Claude <noreply@anthropic.com> Signed-off-by: Robert Kruszewski <robert@spiraldb.com>
0779ba5 to
13eef3b
Compare
Polar Signals Profiling ResultsLatest Run
Powered by Polar Signals Cloud |
Benchmarks: PolarSignals Profiling 📖Vortex (geomean): 1.000x ➖ datafusion / vortex-file-compressed / ns (1.000x ➖, 0↑ 0↓)
No file size changes detected. |
Benchmarks: FineWeb NVMe 📖Verdict: No clear signal (low confidence) How to read Verdict and Engines
datafusion / vortex-file-compressed / ns (1.017x ➖, 0↑ 1↓)
datafusion / parquet / ns (0.983x ➖, 0↑ 0↓)
duckdb / vortex-file-compressed / ns (1.042x ➖, 1↑ 3↓)
duckdb / parquet / ns (0.971x ➖, 0↑ 0↓)
File Size Changes (2 files changed, -46.3% overall, 0↑ 2↓)
Totals:
|
Benchmarks: TPC-H SF=10 on NVME 📖Verdict: No clear signal (environment too noisy confidence) How to read Verdict and Engines
datafusion / vortex-file-compressed / ns (0.990x ➖, 0↑ 0↓)
datafusion / parquet / ns (0.996x ➖, 0↑ 0↓)
duckdb / vortex-file-compressed / ns (0.991x ➖, 0↑ 0↓)
duckdb / parquet / ns (0.986x ➖, 1↑ 0↓)
File Size Changes (9 files changed, -44.0% overall, 0↑ 9↓)
Totals:
|
Benchmarks: Clickbench on NVME 📖Verdict: No clear signal (low confidence) How to read Verdict and Engines
datafusion / vortex-file-compressed / ns (1.003x ➖, 0↑ 0↓)
datafusion / parquet / ns (1.003x ➖, 0↑ 1↓)
duckdb / vortex-file-compressed / ns (1.000x ➖, 1↑ 2↓)
duckdb / parquet / ns (1.007x ➖, 1↑ 2↓)
File Size Changes (101 files changed, -39.2% overall, 0↑ 101↓)
Totals:
|
Benchmarks: Statistical and Population Genetics 📖Verdict: No clear signal (low confidence) How to read Verdict and Engines
duckdb / vortex-file-compressed / ns (1.011x ➖, 0↑ 1↓)
duckdb / parquet / ns (1.003x ➖, 0↑ 0↓)
File Size Changes (2 files changed, -32.3% overall, 0↑ 2↓)
Totals:
|
Benchmarks: Clickbench Sorted on NVME 📖Verdict: No clear signal (low confidence) How to read Verdict and Engines
datafusion / vortex-file-compressed / ns (1.000x ➖, 0↑ 0↓)
datafusion / parquet / ns (1.015x ➖, 0↑ 0↓)
duckdb / vortex-file-compressed / ns (1.047x ➖, 0↑ 1↓)
duckdb / parquet / ns (0.982x ➖, 0↑ 0↓)
File Size Changes (201 files changed, -42.8% overall, 46↑ 155↓)
Totals:
|
Benchmarks: FineWeb S3 📖Verdict: No clear signal (low confidence) How to read Verdict and Engines
datafusion / vortex-file-compressed / ns (1.012x ➖, 0↑ 1↓)
datafusion / parquet / ns (1.065x ➖, 0↑ 0↓)
duckdb / vortex-file-compressed / ns (1.008x ➖, 0↑ 0↓)
duckdb / parquet / ns (0.994x ➖, 0↑ 0↓)
|
Benchmarks: TPC-H SF=1 on NVME 📖Verdict: No clear signal (low confidence) How to read Verdict and Engines
datafusion / vortex-file-compressed / ns (0.998x ➖, 0↑ 0↓)
datafusion / parquet / ns (0.986x ➖, 2↑ 0↓)
duckdb / vortex-file-compressed / ns (1.017x ➖, 0↑ 1↓)
duckdb / parquet / ns (1.004x ➖, 0↑ 0↓)
File Size Changes (9 files changed, -43.9% overall, 0↑ 9↓)
Totals:
|
Benchmarks: TPC-DS SF=1 on NVME 📖Verdict: No clear signal (low confidence) How to read Verdict and Engines
datafusion / vortex-file-compressed / ns (1.022x ➖, 0↑ 2↓)
datafusion / parquet / ns (1.004x ➖, 2↑ 3↓)
duckdb / vortex-file-compressed / ns (0.984x ➖, 11↑ 4↓)
duckdb / parquet / ns (1.003x ➖, 4↑ 6↓)
File Size Changes (25 files changed, -43.5% overall, 0↑ 25↓)
Totals:
|
Rationale for this change
New bottom of the
builders-child-stack. This is the change that makes the rest of the stack pay off, so it lands first.ListViewBuilder::append_listview_arrayrebuilt every incomingListViewArrayinto an exact layout before appending it:Rebasing every offset by the number of elements already in the builder is correct whatever layout the source has, so that rebuild bought nothing but an unconditional promise that the finished array is zero-copyable to a
ListArray. What it cost was any sharing the source had gone to the trouble of expressing, sinceMakeExactis documented as "removing all data overlaps and creating a flattened layout".The case that matters is a constant list array.
constant_canonical_list_arrayalready does the clever thing — "canonicalize only applies to the top level array, so we can simply have 1 scalar in our childelementsand have all list views point to that scalar" — and the builder immediately undid it. Measured on a 10,000-row fill of a 3-element list:That unblocks callers replacing per-row append loops with a single array append —
encodings/sparse'sappend_list_fillcallsappend_array_as_list(fill_elements, ctx)once per filled row, which for a sparse list array of a million rows is a million copies of the same handful of values.What changes are included in this PR?
append_listview_arraykeeps trimming unreferenced elements —ListViewRebuildMode::TrimElementspreserves overlap and the existing flag — but otherwise appends the views as they arrived.ListViewBuildergains azero_copy_to_listfield that startstrueand is cleared when an appendedListViewArraybrings a layout the builder chose not to rewrite.finish_into_listviewreports it instead of hardcodingtrue. Every other append lays its lists down back to back, so only this one path can clear it.The test is deliberately conservative: the result stays zero-copyable only if the incoming views are packed back to back and reference every element they carry, since the next append starts where this one's elements end. Leading or trailing unreferenced elements would leave a gap in the middle of the combined array.
What APIs are changed? Are there any user-facing changes?
No signature changes. Two behavioural ones, both intentional:
ListViewArraynow finishes non-zero-copy-to-list. The flag is per-array and consumers already branch on it (vortex-duckdb/src/exporter/list_view.rs:59has both paths); callers who need an exact layout canrebuild.test_extend_from_array_overlapping_listviewis updated to assert the new contract, including that a null list keeps its source size metadata rather than being rewritten to zero.TrimElementsonly drops leading and trailing ones.uncompressed_size_in_bytes::list_matches_materialized_sizeused a builder round-trip as its proxy for "materialized" and relied on that compaction, so it now compares against an explicitlyMakeExact-rebuilt array; the comment explains why.Checks
cargo test -p vortex-array— 3138 lib tests, including a new one asserting a 10,000-row constant list append stores its value oncecargo test --lib --testsgreen forvortex-arrow,vortex-layout,vortex-scan,vortex-file,vortex-datafusion,vortex-btrblocks,vortex-compressor,vortex-row,vortex,vortex-ipc,vortex-sparse,vortex-fastlanescargo +nightly fmt --all,cargo clippy -p vortex-array --all-targets --all-features,typosGenerated by Claude Code
Stacked PR Chain: builders-child-stack