Exp 263: attribute the per-row memory cost to the right layer - #288
Conversation
Three errors, all flattering the same conclusion in the wrong direction. The headline 2.9x divided the raw 396 B/row marginal by the payload. But a one-column and a six-column read of the same table make the same btree leaves resident, so the id lane's 140 B/row is a per-row storage-side cost independent of how the result is represented. Differencing the projections isolates the Dart side at 256 B/row for the five non-key columns — 1.9x their cell content, not 2.9x. The "~280 B/row theoretical minimum" undercounted: a proper accounting of those five columns is ~296 B (four OneByteStrings at a 24 B header plus rounded data, one boxed double, five values-list slots), against which the measured 256 B/row comes in BELOW. A future runner reading 280 would have inferred ~40% of headroom where there is none. The Row facade should not have been in that accounting at all — the iterator creates those objects transiently and only the ResultSet is retained. The payload denominator averaged the first 100 rows while the lanes read up to 20,000, and both `Item $i` and the description's `$i` grow with the row index: 137.8 B against a true 142.9 B. Adds claim 263.4 for the shared storage-side term — bounded (scales with rows, indifferent to column count) but not discriminated between mmap residency, WAL pages and a cold reader cache — and a nextSignal that a per-row memory figure must difference two projections or it charges the storage engine's work to the row representation. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Belief impactLearned
What this changed
|
There was a problem hiding this comment.
Pull request overview
This PR corrects experiment 263’s narrative and claims by properly attributing the per-row RSS marginal between storage-engine residency and Dart-side row representation, and updates the harness/result writeup to use a correctly averaged payload denominator.
Changes:
- Re-attributes the 396 B/row
selectmarginal by differencing against the 140 B/rowidlane, isolating a 256 B/row Dart-side representation cost and adding a new shared-term claim (263.4). - Updates the experiment writeup/index impact text to reflect the corrected attribution and a revised first-principles floor (~296 B for the five non-key columns).
- Updates the harness and benchmark result markdown to average payload bytes over the full 20,000-row seeded range (142.9 B/row), documenting the prior 100-row-prefix undercount.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| experiments/signals/entries/263.json | Updates changed-beliefs narrative; revises claim 263.1; adds claim 263.4 and a new “difference projections” next-signal. |
| experiments/index/263.json | Updates the experiment index “impact” summary to match the corrected attribution and denominator. |
| experiments/263-select-memory-decomposition.md | Updates the experiment writeup to difference projections and document the shared storage-side per-row term and revised denominator. |
| benchmark/results/2026-08-05T15-30-00Z-exp263-select-memory-decomposition.md | Updates reported avg payload bytes and adds an “Attributing the marginal” section consistent with the new claims. |
| benchmark/experiments/select_memory_decomposition.dart | Changes avg payload computation to average over the full seeded range (but currently does so via per-row string allocation). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Averaging over the full seeded range built every row to measure it — ~100,000 transient strings allocated before any lane runs, ahead of an RSS measurement that cannot see them released. The floor reads 20.5-20.6 MB either way, so nothing moved, but a needless allocation burst in front of a memory probe is a hazard whether or not it fired. Now sampled at a stride of 100 across the range, which captures the digit-width distribution that made a prefix wrong in the first place and lands within 0.01 B of the exact mean while allocating ~140 KB. The sweep results predate the denominator fix entirely: the payload average is an arithmetic property of the fixture, not an input to any measurement. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Correction to #287. No new measurement — same data, three errors in how it was
attributed and divided, all of which happened to flatter the row representation's
overhead in the same direction.
1. The 2.9× charged storage-engine cost to the representation
The headline divided the raw 396 B/row
selectmarginal by the payload. But aone-column and a six-column read of the same table make the same btree leaves
resident — the leaf holds the whole row either way — so the
idlane's 140 B/rowis a per-row cost that has nothing to do with how the result is represented.
Differencing the projections isolates the Dart side:
selectmarginal (6 columns)idmarginal (shared, storage-side)2. The "theoretical minimum" was too low, in the direction that matters
The writeup cited ~280 B/row as the floor. A proper accounting of those five
columns is ~296 B — four
OneByteStrings at a 24 B header (tags + length +hash) plus rounded data (~240 B), one boxed
_Double(16 B), five slots in theflat values list (40 B). The measured 256 B/row comes in below it.
The
Rowfacade should not have been in that accounting at all: it is threefields and a header, but the iterator creates those objects transiently and only
the
ResultSetis retained, so a retained result carries no per-row facade cost.This is the one that actually mattered. A future runner reading "280 B/row
minimum, 396 measured" would infer ~40% of headroom and go looking for it. There
is none — the measurement is already under a first-principles floor.
3. The payload denominator sampled the wrong rows
_payloadBytesaveraged the first 100 rows while the lanes read up to 20,000, andboth
Item $iand the description's$igrow with the row index: 137.8 B againsta true 142.9 B. The harness now averages over the whole seeded range, and the
writeup notes the change so the older figure isn't confusing.
What this changes
The conclusion is unchanged and slightly stronger: the row representation has no
headroom, now confirmed two independent ways — the differenced cost is 0.86× a
first-principles accounting, and the raw marginal is ~0.9× the same accounting
once the storage-side term is included.
Adds claim 263.4 for that shared ~140 B/row term. It is bounded — scales with
rows read, indifferent to column count — but not discriminated: resqlite opens
connections with
mmap_size = 256 MBandcache_size = -8192, so mmap'd filepages becoming resident, WAL pages, and a cold reader page cache are all live
candidates. I've recorded it as a hypothesis rather than a finding, with the cheap
discriminating test (hold row count, widen the row) written down for whoever wants
it.
And a
nextSignal: difference two projections before quoting a per-row memorycost, or you charge the storage engine's work to the row representation — which is
exactly how the first revision got 2.9×.
Test plan
dart analyze --fatal-infoson the harness — cleanavg_payload_bytes_per_row=142.9check_experiment_signals.dartvalid;finalize_experiment.dartgreen;check_knowledge_links.dartclean (75 claims)lib/,native/orhook/changes.🤖 Generated with Claude Code