perf(array): accumulate nested builder validity without a null buffer [builders-child-stack] - #8966
Conversation
85f8dcd to
b67473e
Compare
Merging this PR will degrade performance by 32.39%
Warning Please fix the performance issues or acknowledge them on CodSpeed. Performance Changes
Tip Investigate this regression by commenting Comparing Footnotes
|
b67473e to
8561525
Compare
A nested builder learns about validity from two sources: one row at a time as scalars are appended, and a whole array's worth at a time as arrays are. Only the first needs a null buffer, but `LazyBitBufferBuilder` treated both the same, so every appended array had its validity executed into a `Mask` and its bits copied. `ValidityBuilder` keeps a whole array's validity as a run and concatenates the runs at the end, the way `Validity::concat` already does for `StructArray::try_concat`. `AllValid` and `AllInvalid` runs cost nothing, array-backed runs are bool arrays that are already built, and a builder that only ever saw uniform validity still answers from its nullability rather than producing a bool array. However few values a run covers, it is kept as it arrived, so a builder's validity is split on exactly the boundaries its children are. `StructBuilder`, `ListBuilder`, `ListViewBuilder` and `FixedSizeListBuilder` use it; the leaf builders keep `LazyBitBufferBuilder`. 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>
8561525 to
a8ebf30
Compare
Rationale for this change
Stacked on #8964 — review that first.
A nested builder learns about validity from two sources: one row at a time as scalars are appended, and a whole array's worth at a time as arrays are. Only the first needs a null buffer.
LazyBitBufferBuildertreated both the same, so every appended array paid— executing the array's validity into a
Maskand copying its bits into a running buffer.StructArray::try_concathas never done that. It hands its per-chunk validities toValidity::concat, which returnsAllValid/AllInvalid/NonNullablewhen they are uniform and aValidity::Array(ChunkedArray<Bool>)otherwise — no bitmap, no execution. That gap is the one thing standing between the builders and the swizzle helpers they are meant to replace: routing chunked struct canonicalization throughStructBuildertoday would trade a lazy validity for a materialized one on every nullable chunked struct, which is the kind of regression that shows up in a scan rather than in a test.What changes are included in this PR?
ValidityBuilder(vortex-array/src/builders/validity.rs) mirrorsChildBuilder: appended validity is kept as a run, bits appended one at a time go into aLazyBitBufferBuilder, andfinishflushes the buffer into the run list and concatenates.StructBuilder,ListBuilder,ListViewBuilderandFixedSizeListBuilderhold their validity this way; the leaf builders keepLazyBitBufferBuilderunchanged.The call sites lose their
execute_mask:append_validityneeds neither theExecutionCtxnor aVortexResultany more, since nothing is executed on this path.Two details worth calling out for review:
Validity::concattreatsNonNullableandAllValidas different kinds and falls back to a bool array when both appear — which happens as soon as a non-nullable array is appended next to a scalar. Both mean "no nulls", sofinish_with_nullabilitychecksdefinitely_no_nullsacross the runs first and answers from the declared nullability instead. Without that, appending an array and then a scalar to a non-nullable builder produced aValidity::Arrayand tripped the nullability assertion.Seven unit tests on
ValidityBuilder(runs preserved, uniform runs collapsing, a one-row validity earning a run too, bits and runs interleaving in order,set_validityreplacing runs, non-nullable finishing non-nullable, finish resetting), plus a builder-level test asserting that two appended nullable struct arrays come back with a chunked bool validity rather than one copied buffer.What APIs are changed? Are there any user-facing changes?
None —
ValidityBuilderispub(crate). The observable change is representational: a nested builder that consumed nullable arrays now returnsValidity::Array(ChunkedArray<Bool>)where it previously returned a singleBoolArray. That is the same shapeStructArray::try_concathas always produced.Checks
cargo nextest run --workspace— 7168 passed, 560 skippedcargo clippy --all-targets --all-features— cleancargo +nightly fmt --all --check— cleanGenerated by Claude Code
Stacked PR Chain: builders-child-stack
Generated by Claude Code