Rework AggregateVTable - #9816
Conversation
Merging this PR will regress 1 benchmark
|
| Mode | Benchmark | BASE |
HEAD |
Efficiency | |
|---|---|---|---|---|---|
| ❌ | WallTime | filtered_owned_i64_avx2[OneNullInEight] |
22.4 µs | 26 µs | -13.92% |
| ⚡ | WallTime | filtered_owned_i64_avx512[OneNullInEight] |
26.3 µs | 22.4 µs | +17.6% |
Tip
Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.
Comparing claude/aggregate-fn-vtable-refactor-ny5l4s (6ed6e20) with develop (ad925fd)
Footnotes
-
218 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports. ↩
de73390 to
0a82483
Compare
87b41dd to
798246e
Compare
…duce_partials The vtable's `empty_partial`, `combine_partials` (typed partial plus untyped scalar) and `reset` are replaced by two primitives so each aggregate either parses scalars or operates on typed state: - `partial_from_scalar(options, input_dtype, scalar) -> Partial` parses a partial scalar (kernel results, cached statistics, other accumulators' `to_scalar`) into the typed state, and is its inverse. - `reduce_partials(options, input_dtype, impl IntoIterator<Item = Partial>)` reduces owned partials in iteration order; the empty sequence is the identity (the state of a group with no values). Accumulator changes: - `Accumulator.partial` is an `Option`, with `None` as the cheap empty state, so folds and empty accumulators never construct an identity partial. - `DynAccumulator::combine_partials` is removed. `merge_from` downcasts the other accumulator (`DynAccumulator::downcast_mut`), checks that the aggregate, options and input dtype match, and folds its typed partial directly, so merging never round-trips through scalars. - `Combined` holds typed child accumulators and merges children with `merge_from`; its `reduce_partials` seeds from the first pair instead of building fresh children per fold. Partial states: - Min, Max, MinMax, BoundedMin, BoundedMax, IsSorted, IsConstant, Sum and SumV2 build their identity from one `empty` constructor, and reduce bodies move values instead of cloning them. - IsSorted and IsConstant no longer serialize a false verdict without a first value as the null (empty) struct, which previously lost the verdict when merged into a materialized empty state or persisted as a zone stat. IsSorted's reduce honors an unsorted partial before the emptiness check. - BloomPartial gains a typed `union`, so reducing bloom partials ORs blocks without serializing. `can_satisfy` documents that satisfaction is a claim about stored state: a partial for the requested aggregate must be creatable from this aggregate's partial. DuckDB's `finalize_scan` merges thread-local accumulators with `merge_from`. Tests construct partials directly where parsing is not the subject, and cover merging into a materialized empty state, boundary-less false verdicts, type-erased Combined merges, and SumV2 identity/overflow merges. Signed-off-by: "Claude" <noreply@anthropic.com> Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BAC54whRD3iHLBZfM4TCvd
Every AggregateFnVTable execution method now receives the bound options and the resolved input/partial/result dtypes, so partial states only hold accumulated values instead of copies of the options and dtypes. - Add `AggregateDTypes<'a>` (borrowed) and `OwnedAggregateDTypes`, resolved once by `Accumulator`/`GroupedAccumulator` and lent to every vtable call. - Split reduction into typed primitives: `empty_partial` (the identity) and binary `merge_partials`; `reduce_partials` becomes a provided fold. - Add `DynAccumulator::combine_partial_scalar`, which parses a partial scalar and merges it through the typed vtable in one monomorphized call; `Accumulator` uses the same path for kernel results and cached statistics. - Strip options/dtype fields from every partial (Sum, SumV2, Count, Min/Max/MinMax, BoundedMin/Max, IsSorted, IsConstant, First/Last, BloomPartial's hash_fn, ...). `Count`'s partial is now a plain `u64`. - `BinaryCombined::return_dtype`/`finalize`/`finalize_scalar` take the combined options and dtypes; `Mean` derives its target dtype from them. Signed-off-by: "Claude" <noreply@anthropic.com> Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BAC54whRD3iHLBZfM4TCvd
Renames the resolved-dtype pair so the owned type carries the plain name and the borrowed view is suffixed: - `OwnedAggregateDTypes` -> `AggregateDTypes` - `AggregateDTypes<'a>` -> `AggregateDTypesRef<'a>` Its fields and accessors keep the names they had on the accumulators they were lifted from: `dtype`, `return_dtype`, and `partial_dtype`. Also removes `AggregateFnVTable::reduce_partials`. It was a provided fold over `empty_partial` and `merge_partials` with no non-test callers, so the tests now merge directly, which exercises the binary operation rather than the fold wrapper. Merging with the empty partial is the identity, so a two-element reduction is exactly one `merge_partials`. Signed-off-by: "Claude" <noreply@anthropic.com> Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BAC54whRD3iHLBZfM4TCvd
… fields `DynAccumulator::combine_partials` took a single `Scalar`, so the plural was misleading; the sibling `merge_from` already distinguishes combining another accumulator from combining one partial scalar. `AggregateDTypes` now exposes `dtype`, `return_dtype` and `partial_dtype` as public fields, matching `AggregateDTypesRef`, and drops the three accessors that only returned them. `try_new` and `borrow` stay, since they resolve the dtypes through the vtable and lend them out rather than just reading a field. Signed-off-by: "Robert" <robert@spiraldb.com> Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BAC54whRD3iHLBZfM4TCvd
Tests that needed an empty partial were spelling out the identity state by hand, restating in the test what each vtable already defines. They now call `empty_partial`, so a change to an aggregate's identity state cannot leave a test asserting against a stale hand-written one. Covers First, Last, BoundedMin, BoundedMax, GeometryAabb and BloomFilter. First and Last no longer touch their partial structs at all: the non-empty cases go through `partial_from_scalar`, which for both is exactly a wrapped non-null scalar. `Accumulator::empty_partial` becomes `pub(crate)`, matching the existing visibility of `fold_partial`, so the bounded min/max tests can fold an empty partial without rebuilding the dtypes. Left alone: the is_sorted and is_constant partials, whose tests assert a non-empty verdict is distinguishable from the empty state and so must build it directly; the non-identity partials in sum, min_max and the decimal sum tests; and the BloomPartial unit tests, which exercise the partial type itself rather than the aggregate. Signed-off-by: "Robert" <robert@spiraldb.com> Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BAC54whRD3iHLBZfM4TCvd
`From<Vec<[u32; 8]>>` existed only so one test could hand-build a saturated filter, which also let that partial disagree with its own options: it carried four blocks while the options said 256. The test now builds the filter through `partial_from_scalar` from an all-ones buffer sized to the options' block count, so the partial is consistent with the options it is checked against and nothing outside the module can bypass `BloomPartial`'s invariants. Signed-off-by: "Robert" <robert@spiraldb.com> Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BAC54whRD3iHLBZfM4TCvd
798246e to
6ed6e20
Compare
Most methods now also take options thus partial state doesn't need to persist options necessary on each invocation. We separate out parse/merge operation on partial state such that we can perform operations in Partial type and also can easily validate serialisation of Partial into scalar