perf(array): forward probe_scalar through pass-through encodings - #9905
joseph-isaacs wants to merge 1 commit into
Conversation
Slice, Dict, Chunked, Shared, Masked, Extension, FoR, ZigZag, ALP, FSST and DateTimeParts cost almost nothing per row themselves, but each still served reads from the default `probe_scalar`, which forwards to `scalar_at` and reads children with a one-off `execute_scalar`. Any retained preparation below them was rebuilt on every row, so an encoding that keeps decoded state only paid off at the root of a tree. Each now implements `probe_scalar` once and reads its children through `state.slot(..)`, with `scalar_at` delegating via `ProbeState::once` so one-off and repeated reads share a body. Two need more than a slot read: Shared resolves to either its source or its computed cache and rebuilds its probe when that changes, and FSST rebuilds its codes array from a buffer and the offsets slot, so it retains one probe over that array. ALP's patched path still reads through `Patches`; only its unpatched read is probed. Adds a conformance test asserting a retained probe agrees with one-off reads over a backwards, forwards and sparse visit order, so every encoding in the consistency suite covers its probe path. Signed-off-by: "Joe Isaacs" <joe.isaacs@live.co.uk> Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ui7E5mtZLgh4j41ugrLXbS
Merging this PR will regress 2 benchmarks
|
| Mode | Benchmark | BASE |
HEAD |
Efficiency | |
|---|---|---|---|---|---|
| ❌ | WallTime | filtered_owned_i64_avx512[OneNullInEight] |
22.8 µs | 26.7 µs | -14.52% |
| ❌ | WallTime | mul_u32_nonnull_avx512 |
5.6 µs | 6.2 µs | -10% |
| ⚡ | WallTime | arrow_checked_add_u32_neon[16384] |
20.4 µs | 13.5 µs | +51.37% |
| ⚡ | WallTime | filtered_owned_i64_neon[NineNullsInTen] |
18.9 µs | 15.7 µs | +20.17% |
| ⚡ | WallTime | filtered_sink_i64_neon[NineNullsInTen] |
18.8 µs | 15.7 µs | +19.47% |
| ⚡ | Simulation | allocate_drop_arrow[0] |
456.9 ns | 402.7 ns | +13.45% |
| ⚡ | Simulation | allocate_drop_bytes[0] |
575.7 ns | 521.6 ns | +10.39% |
Tip
Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.
Comparing ji/epic-euler-88rquc (1157ea3) with develop (4af09a9)
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. ↩
Summary
Eleven encodings that cost almost nothing per row themselves were still serving reads from the default
probe_scalar, which forwards toscalar_atand reads children with a one-offexecute_scalar. Preparation retained below them was rebuilt on every row, so an encoding that keeps decoded state only paid off when it sat at the root of a tree. A column stored asDict(Pco)orChunked(Pco)threw the decoded page away between reads; the same PCO array read directly kept it.Each of
Slice,Dict,Chunked,Shared,Masked,Extension,FoR,ZigZag,ALP,FSSTandDateTimePartsnow implementsprobe_scalaronce and reads its children throughstate.slot(..), withscalar_atdelegating viaProbeState::onceso one-off and repeated reads share a body. None of them needs encoding state; the child probes the repeated probe already keeps are enough.This is independent of #9844 and can land in either order, but the two compose: this PR carries retention across a parent, #9844 gives the children something worth retaining.
Changes
vortex-array:Slice,Dict,Chunked,Shared,MaskedandExtensionread their children through the probe's slots.Chunkedresolves the chunk first and probes only that slot, so a probe walking one chunk keeps that chunk's preparation and never touches the others.encodings:FoR,ZigZag,ALP,FSSTandDateTimePartslikewise.DateTimePartsreads its three parts through one helper rather than three copies of the same chain.Sharedresolves to either its source or its computed cache, and the swap can happen between reads, so its state holds a probe and rebuilds it when the array it was built over is no longer current.FSSTrebuilds its codesVarBinArrayfrom a buffer and the offsets slot on every read, so its state holds one probe over that array instead.ALP's patched path still reads throughPatches::get_patched; only the unpatched read, the common one, goes through the probe. Worth a follow-up alongside the otherPatchescallers.test_repeated_probe_consistencyin the conformance suite asserts a retained probe agrees with one-off reads over a backwards, then forwards, then sparse visit order, so a cache that outlives its row or a slot read through the wrong index fails there. It runs for every encoding already wired intotest_array_consistency, which covers these eleven and guards future migrations.Measurements
The same PCO array wrapped in one parent, 65,536 rows, 1024-value pages, 64 random lookups per pass, release build on a 4-core x86_64 cloud box. Both columns have #9844 merged in, so the only difference is this PR:
Each figure is a one-off read divided by a repeated-probe read, clustered lookups inside a four-page window. The
pcoandrunend(pco)rows are unwrapped controls and do not move; the three wrapped rows go from no benefit at all to within reach of the unwrapped array. Scattered lookups over the whole array show the same shape at smaller ratios, since they rarely hit a page twice whatever retention is in place.On
developalone this change is flat, as it should be: onlyPrimitiveandStructimplementprobe_scalarthere, and neither has state worth keeping. What it buys today is that a child's validity resolution is no longer redone per row; what it unlocks is every encoding that retains decoded state.Validation:
cargo testforvortex-array,vortex-fsst,vortex-alp,vortex-zigzag,vortex-fastlanesandvortex-datetime-parts(4402 tests, 0 failures), andcargo fmtwith the pinned nightly. Workspace clippy was not run.🤖 Generated with Claude Code
https://claude.ai/code/session_01Ui7E5mtZLgh4j41ugrLXbS
Generated by Claude Code