Exp 258: columnar typed-array result store (moonshot, rejected) - #276
Merged
Conversation
Reopens exp 055's assessed-but-never-implemented columnar result store and gives the first real AOT measurement of the typed-array mechanism. Publication-only: focused harness + receipt + writeup + signal/index fragments; no lib/native change. Columnar builds 60-90% faster worker-side (no double-boxing) and its transfer is 73-91% cheaper on a SendPort A/B, but the large results where transfer wins most already zero-copy via the Isolate.exit sacrifice path (>32768 slots), and sub-threshold SendPort lanes save only tens of microseconds (below the round-trip floor). REAL consume is neutral; memory is a wash-to-regression (inline Smis beat Int64List). The one real primary-metric signal was ~2x faster main-isolate integer consume from Int64List locality, recorded as a scoped candidate. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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.
Moonshot · publication-only (harness + writeup + fragments; no
lib//native/change) · experiments/258-columnar-result-store.mdHypothesis
Every
select()result is backed by one flat, row-majorList<Object?>:decodeQueryboxes every numeric cell into it, the list crosses the reader→main isolate boundary, andRow['col']reads the boxed value back out on the main isolate. Exp 224 recently closed the FFI-crossing axis of this path and, in doing so, named the real remaining cost out loud — "Dart object construction," i.e. the boxing. The obvious structural answer is per-column typed arrays (Int64List/Float64Listfor numeric columns), which exp 055 proposed back in April but assessed and never implemented: it only estimated the transfer win as "too small," and said the memory win needed a profiling methodology that didn't exist yet.Two things changed since then, which is why this was worth a bounded pass now: that memory methodology now exists (
ProcessInfo.currentRss+ the RSS diagnostics from exp 174/183, and the process-isolated transfer harnesses from exp 244/245), and exp 224 gave direct evidence that object construction — not the FFI crossing — is the top rows-path cost. The bet: storing numeric columns as typed arrays lets the worker skip boxing during decode, the container cross the isolate boundary as amemcpyinstead of a boxed structured-clone, and the main isolate box lazily only on the cells a caller reads.Approach
A focused, AOT-compiled A/B harness (
benchmark/experiments/columnar_result_transfer.dart) feeds a flat boxedList<Object?>and a columnar typed-array container identical raw numeric source data, and measures three costs separately across a real worker→mainSendPorthop, over two order-flipped passes: build (worker wall — flat boxes, columnar doesn't), hop (main-observed round trip = build + serialize + structured-clone receive), and consume (main-isolate full scan, reading every cell asObject?the wayRow['col']does). It deliberately doesn't stand up SQLite — the goal is to isolate the container mechanism 055 estimated and never ran, not to re-measure the decode loop exp 224 already covered.The load-bearing production detail the harness encodes: a reader sacrifices (hands its heap to main via
Isolate.exit, zero-copy) once a result exceeds 32768 structural slots. So lanes are tagged(send)(under the threshold → realSendPortcopy) vs(exit)(over it → production transfers for free), because a columnar transfer win only reaches production on the(send)lanes.Results
Δ = columnar vs flat (negative = columnar faster), both order passes;
NET = hop+consis the main-isolate figure.Signs are stable across the flip, so these are real — but the mechanism splits three ways once mapped to production. (1) The build win is large (60–90%, ~10× on wide REAL — doubles always box) but it's worker wall, off the main isolate. (2) The headline transfer win doesn't reach production where it's largest: the
(exit)lanes already zero-copy viaIsolate.exit, so columnar'smemcpycompetes with free; on the(send)lanes where it does apply, the absolute saving is tens of microseconds (31 µs → 14 µs), below theselect()round-trip floor exp 105 established. (3) One surprise — main-isolate integer consume is ~2× faster (Int64Listlocality vs pointer-chasing aList<Object?>ofSmis), while REAL consume is only neutral (box-on-access cancels the gain) but never regresses. Memory is a wash-to-regression:Smiints live inline in aList<Object?>, so anInt64Listcolumn costs more, not less.Outcome
Rejected — don't rewrite the
ResultSetbacking store to columnar. The one axis that would justify touching resqlite's hottest, most-tested path (main-isolate time) isn't moved in the general case: the transfer win is neutralized by the existing sacrifice path for large results and sits below the round-trip floor for small ones, REAL consume is neutral, and memory doesn't improve. This completes exp 055's open assessment with the first real measurement of the typed-array mechanism and confirms exp 081's main-isolate-access caution generalizes.Reopen only if a workload profile shows integer-heavy, main-isolate-bound reads dominating — the one place columnar showed a real primary-metric signal (~2×
Int64Listconsume). That's a narrow numeric-column path, not the whole-store rewrite; it needs its own design pass (howRowdispatches columnar-vs-flat per column without slowing text), so it's recorded as a scoped, out-of-budget candidate in the signal map, backed by this measurement.Test plan
dart analyze --fatal-infos benchmark/experiments/columnar_result_transfer.dartbenchmark/results/2026-07-29T11-23-43Z-exp258-columnar-result-transfer.mddart run benchmark/finalize_experiment.dart --experiment=experiments/258-columnar-result-store.md(green)dart run benchmark/check_experiment_signals.dart(valid)lib//native//hook/changes — publication-only