Skip to content

perf(array): forward probe_scalar through pass-through encodings - #9905

Open
joseph-isaacs wants to merge 1 commit into
developfrom
ji/epic-euler-88rquc
Open

joseph-isaacs wants to merge 1 commit into
developfrom
ji/epic-euler-88rquc

Conversation

@joseph-isaacs

Copy link
Copy Markdown
Contributor

Summary

Eleven encodings that cost almost nothing per row themselves were still serving reads from the default probe_scalar, which forwards to scalar_at and reads children with a one-off execute_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 as Dict(Pco) or Chunked(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, FSST and DateTimeParts 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. 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, Masked and Extension read their children through the probe's slots. Chunked resolves 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, FSST and DateTimeParts likewise. DateTimeParts reads its three parts through one helper rather than three copies of the same chain.
  • Two need more than a slot read. Shared resolves 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. FSST rebuilds its codes VarBinArray from 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 through Patches::get_patched; only the unpatched read, the common one, goes through the probe. Worth a follow-up alongside the other Patches callers.
  • Tests: test_repeated_probe_consistency in 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 into test_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:

array #9844 only with this PR
pco 13.3x 12.6x
slice(pco) 1.0x 16.6x
dict(pco) 1.0x 10.1x
chunked(pco) 1.0x 14.1x
runend(pco) 6.6x 6.4x

Each figure is a one-off read divided by a repeated-probe read, clustered lookups inside a four-page window. The pco and runend(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 develop alone this change is flat, as it should be: only Primitive and Struct implement probe_scalar there, 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 test for vortex-array, vortex-fsst, vortex-alp, vortex-zigzag, vortex-fastlanes and vortex-datetime-parts (4402 tests, 0 failures), and cargo fmt with the pinned nightly. Workspace clippy was not run.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Ui7E5mtZLgh4j41ugrLXbS


Generated by Claude Code

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
@joseph-isaacs joseph-isaacs added the changelog/performance A performance improvement label Sep 16, 2026 — with Claude
@codspeed

codspeed Bot commented Sep 16, 2026

Copy link
Copy Markdown

Merging this PR will regress 2 benchmarks

⚠️ Unknown Walltime execution environment detected

Using the Walltime instrument on standard Hosted Runners will lead to inconsistent data.

For the most accurate results, we recommend using CodSpeed Macro Runners: bare-metal machines fine-tuned for performance measurement consistency.

⚠️ Different runtime environments detected

Some benchmarks with significant performance changes were compared across different runtime environments,
which may affect the accuracy of the results.

Open the report in CodSpeed to investigate

⚡ 5 improved benchmarks
❌ 2 regressed benchmarks
✅ 2195 untouched benchmarks
⏩ 218 skipped benchmarks1

Warning

Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

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)

Open in CodSpeed

Footnotes

  1. 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

changelog/performance A performance improvement

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant