perf(array): append repeated and viewed lists in bulk, not a value at a time [builders-child-stack] - #9064
Open
robert3005 wants to merge 1 commit into
Conversation
This was referenced Jul 29, 2026
Closed
Merging this PR will degrade performance by 83.3%
Warning Please fix the performance issues or acknowledge them on CodSpeed. Performance Changes
Tip Investigate this regression by commenting Comparing Footnotes
|
… a time Four callers walked a builder one list at a time where a whole run of them was available up front. `Sparse` canonicalization filled the gaps between patches by appending the fill value once per row, and appended the patches themselves one list at a time. A gap is a run of one value, so it goes in as a single `ConstantArray`: canonicalizing a constant list array points every view at one copy of the value, so a gap now costs the fill value's elements once however many rows it covers. Appending a 10,000-row gap of a three-element fill produced 30,000 elements; it now produces 3. Patches landing on consecutive rows go in as one slice of the patch array, so the builder sees an append per gap rather than one per patch. The patch values are flattened once up front instead. That is what lets a run be sliced out of them, and it also means a null patch carries a zero-size view rather than the elements the old code skipped, so the appended run holds exactly the elements the patches reference. `ListBuilder::append_listview_array` sliced the elements array and appended the slice once per list, which is what its `ListViewBuilder` twin stopped doing. `ListArray` offsets can only describe contiguous, in-order lists, so flatten the incoming views to that layout - a no-op when they are laid out that way already - and then append the referenced elements in one go, walking only the metadata to rebase the offsets. Signed-off-by: Claude <noreply@anthropic.com> Signed-off-by: Robert Kruszewski <robert@spiraldb.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
robert3005
force-pushed
the
claude/builders-bulk-loop-callers-9ze0t6
branch
from
July 29, 2026 14:19
b622d0f to
3304731
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Four callers walked a builder one list at a time where a whole run of them was available up front.
Sparsecanonicalization — the gaps. The fill value was appended once per row. A gap is a run of one value, so it goes in as a singleConstantArrayinstead: canonicalizing a constant list array points every view at one copy of the value, so a gap now costs the fill value's elements once however many rows it covers. Appending a 10,000-row gap of a three-element fill produced 30,000 elements; it now produces 3. The capacity estimate follows, bounding the fill by the number of gaps rather than the number of filled rows.Sparsecanonicalization — the patches. The patch loop appended one list per patch. Patches landing on consecutive rows now go in as one slice of the patch array, so the builder sees an append per gap rather than one per patch. This is the case that matters once #8964 lands: with 100 patches on consecutive rows, the elements child went from 101 chunks to 2.To slice a run out of the patch values they have to be laid out in row order, so they are flattened once up front with
MakeExactrather than a list at a time inside the loop. That has two further effects worth noting for review: a null patch now carries a zero-size view instead of the elements the old code skipped, so an appended run holds exactly the elements its patches reference; and the elements half of the capacity estimate becomes exact rather than an upper bound.ListBuilder::append_listview_arraysliced the elements array and appended the slice once per list, which is what itsListViewBuildertwin stopped doing.ListArrayoffsets can only describe contiguous, in-order lists, so flatten the incoming views to that layout — a no-op when they are laid out that way already — and then append the referenced elements in one go, walking only the metadata to rebase the offsets.This is groundwork for the rest of the stack: once a nested builder keeps every appended array as a chunk, a caller appending a value at a time would produce a chunk per row.
What is still per-row
Patches that alternate with fill rows (
indices = [0, 2, 4, …]) still cost one append each, and there is no coalescing to be had — those rows genuinely alternate between patch values and fill values, so the elements child genuinely interleaves. Only a copy could flatten that, which is the trade #8964 leaves to the caller.Tests
Two new tests in
encodings/sparse, each checked against a mutated implementation:MakeExactflatten fails this test and nothing else; using the old per-patchnext_indexformula fails it along with five existing ones.One in
vortex-array: an overlappingListViewArrayand a sliced (leading-slack) one appended into the sameListBuilder.Checks
cargo nextest run --workspace— 7168 passedcargo clippy --all-targets --all-features— cleancargo +nightly fmt --all --check— cleancargo test --doc -p vortex-array -p vortex-sparse— passed🤖 Generated with Claude Code