elixir #174: lever M — one binary construction per barrier - #207
Conversation
…ers as several segments Lever A closed the merge group across the fields of a scope and K closed it across the elements of a clause, but neither could close it across the GROUP's own budget: a barrier-free run wider than 52 bits had to empty the scratch to keep merging, and every emptying was its own bs_append. A bench_mixed header is 82 static bytes and paid ten of them; an entity element paid three. The emptying and the append were never the same thing. A full group now SPILLS — the scratch's whole bytes are bound to a fresh register and the scratch keeps merging — and the append is deferred to the barrier, where every register that spilled since the last one rides ONE binary construction as its own segment. A spill is a bind and a shift where it was an append and a shift, so the statement count is what it was and only the BIF calls move. This is issue #174's own mechanism, and its micro (recorded on #202) is the direct evidence: one append of twelve segments costs 73.3 ns where twelve appends of one segment cost 103.6, 1.41x, and the same twelve appends with a DYNAMIC segment width cost 103.7 — identical. The append is the expense. So a dynamic-offset region (a loop helper's body, where scratch_bits is the wire's) defers exactly the same way, each spill naming the width it computed. The write clause's k follows from it. What a clause shares is one APPEND, and the append is now the barrier's rather than the group's, so the 52-bit budget no longer bounds how many elements a clause takes — only the unroll cap of four does: stats (18 bits): 2 elements per clause -> 4; 40 appends -> 20 loadout (8 bits): 4 per clause, already the cap; 1 append entities (135 bits/element): 3 appends per element -> 1 bench_mixed's header: 10 appends -> 1 Static append sites in generated/bench/elixir: 79 -> 44. The fixnum envelope is untouched, and that is the constraint this whole round rests on. mergeW's guard is unchanged, so a group is still at most 52 bits and the scratch still at most 7 + 52 = 59; a register is filled by exactly the spill that used to append it, so it holds what the scratch held and nothing is ever grown. The registers stay live to the barrier instead of dying at the append — the deepest run in the tree spends nine of the BEAM's 1024. Bytes and behaviour are unchanged. The segment takes the low whole bytes, so the residual riding above them is simply not in the segment and needs no masking; the statements are the same ones in the same order over the same values; every range, count, mask and length check is where it was; and the raise TEXT is held by the display map exactly as under K — a widened clause names slots e1..e4 and a message still reads "e.delta is above the wire maximum". The 34 raise sites the diff ADDS are the new slots, and it removes none. The barrier's construction is emitted in mix format's own shape for a long bitstring — broken after the `=`, `<<` at ind+2, continuations at ind+4, each segment measured with the comma or the closing `>>` that follows it — so `mix format --check-formatted` passes on the emitted text. Wire bytes unmoved: no file under testdata/wire is touched, `make update-goldens` rewrote every wire golden and every one came back byte-identical, corpus_id 6b213fbfa1a03a99, the full variant round-trip gate green, test/elixir and test/elixir-ludicrous OK — including Clauses.schema and Joins.schema, whose mid-byte clause boundaries are exactly where a grouped write can disagree with a grouped read. Every generated module compiles under --warnings-as-errors, the nine-backend `make test` chain is green, regeneration is zero-diff, shape-gate is clean with no new ledger entries, and mix format, modernize, gofmt and go vet are clean. The seven SOURCE goldens the emitter moved are re-pinned here. MEASURED: in the sitting's A/B, recorded on the PR with the round's table.
A paired same-sitting --quick A/B on the fanless M2 Air, the two halves two minutes thirty-nine seconds apart, corpus_id 6b213fbfa1a03a99 on every row. BEFORE stamps main at d398ec4, AFTER stamps this branch at f96d749, so both resolve on origin and reconstruct the tree that was measured. elixir, bench_mixed, family gen, max rates: write 383,329 -> 434,318 msg/s 1.133x (spread 0.20% -> 0.25%) round_trip 248,616 -> 254,142 msg/s 1.022x (spread 0.55% -> 0.50%) as a share of generated C++ (§2.9): 1413% -> 1409% And the controls say to read the second row as nothing. The AFTER half was globally the faster of the two — EVERY one of the eight non-elixir legs came in up, +1.4% to +4.1%, cpp round_trip itself +1.9%. Elixir's write at +13.3% is well clear of that; its round_trip at +2.2% is inside it, and net of cpp the blend moves 0.3%. That is not evidence the lever does nothing to round_trip. It is evidence this instrument cannot resolve the question: one sweep is ONE sample per arm, and elixir's round_trip rate varies far more BETWEEN runs in a sitting than within one — across nine interleaved passes of the same main-branch code it ran 219,874 to 247,320, a 12.5% range, against the 0.55% spread inside a single run. A 6% effect does not survive that. So the lever's own figure comes from the interleaved instrument levers K and L used, which puts both arms in the same window and rotates their order. It is recorded on the PR and reproduced in the lever's commit message; this pair is committed for the cross-language context and for the honest record of what the sweep did and did not show. The sitting is labelled SHARED in both preambles, which it was: a sibling session's C# round was live on the same box.
…iew asked for The join reset cleared pendW/scZero/sbKnown/sbVal/scBound/sbBound but not segW, relying on every arm ending in a flush. A future arm that legitimately does not barrier would split the failure mode: registers spilled before the join re-emit at the next barrier (silently — the wire-corruption class), registers spilled inside an arm fail to compile. One line closes the class. segN stays monotonic on purpose: resetting it renumbers registers after every join, churning generated text for no safety. Regeneration zero-diff. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Review verdict: MERGE, all conditions now landed. The reviewer verified the spill machinery structurally (a Go flattener rendering every multi-segment construction back into the append sequence it replaced — every static byte-width sequence identical to main, every dynamic sequence identical in count and order), ran its OWN negative control (reversed segment order -> six checks red including the C++-pin byte compare), and worked the fixnum envelope by hand (the same 59-bit ceiling main already hit, not raised). It also DISSOLVED the derived-read anomaly: an arithmetic artifact by construction (round_trip minus write, ±30% band from round_trip's own 12.5% variance, three instruments sharing one number), with M provably read-neutral — the body's paragraph is rewritten accordingly. Conditions landed: the false control sentence corrected in place with the correction named (commit 894414a's message carries the old claim — noted rather than rewritten, history stays); the segW join-reset guard (one line, zero-diff regen, segN kept monotonic to avoid renumber churn); nine->13 registers; the wire-gate claim restated in the terms that actually carry an Elixir-only diff. Estate-wide finding routed to #194: §2.9 F1's datadriven filename rule is followed by no CSV in the repo including main's own — the ledger issue owns sub-era bookkeeping and should either enforce or amend it. |
Issue #174. Lever M — the one lever #202 named, prototyped and did not
land. With the NIF backend ruled out, it is the last planned Elixir lever, so
the number it lands is the BEAM's published floor for now.
Headline, and it is below the estimate. M makes the write path
1.17x faster and moves the §2.9 blend somewhere between 0% and 6%,
against the ~12% #202 estimated. The write half is unambiguous and measured
by two instruments; the blend half is small, and the certification pair could
not resolve it at all. Both are reported as measured, neither is smoothed.
What M is
Lever A closed the merge group across the fields of a scope and K closed it
across the elements of a clause, but neither could close it across the
GROUP's own budget: a barrier-free run wider than 52 bits had to empty the
scratch to keep merging, and every emptying was its own
bs_append. Abench_mixedheader is 82 static bytes and paid ten of them; an entityelement paid three.
The emptying and the append were never the same thing. A full group now
spills — the scratch's whole bytes bind to a fresh register and the
scratch keeps merging — and the append is deferred to the barrier, where
every register that spilled since the last one rides ONE binary construction
as its own segment. A spill is a bind and a shift where it was an append and
a shift, so the statement count is unchanged and only the BIF calls move.
The dynamic-offset path (a loop helper's body) defers identically, each spill
naming the width local it computed — #174's own micro says that costs
nothing: twelve appends of a DYNAMIC-width segment measured 103.7 ns against
103.6 for literal widths.
The write clause's
kfollows from it. What a clause shares is one APPEND,and the append is now the barrier's rather than the group's, so the 52-bit
budget no longer bounds
k— only the unroll cap of four does.generated/bench/elixirbench_mixedheaderMeasured
The lever's own figure comes from the interleaved instrument levers K and
L used: both arms in the same window, order rotated per pass, so a spike hits
both. Two sittings,
bench/elixirunder the pinned BEAM toolchain, the samerunner and the same corpus over two generated
Bench.extexts, max statistic,median of per-pass ratios.
Ten passes, main vs M — M won 9 of 10 on write:
Nine passes, three-way, separating M's two halves — C is the deferred
appends alone, with the clause
kleft where K had it:Both halves earn their place: deferring the appends is 1.10x on write on its
own, and widening the clause adds a further 1.07x. Neither is scaffolding.
The certification pair, and what it could not show
bench/run.sh --quick, both halves 2m39s apart, corpus_id6b213fbfa1a03a99on every row, both committed:
bench/results/2026-09-01-elixirleverm-{before,after}-quick-arm64-macbook.csv.BEFORE stamps main at
d398ec4, AFTER stamps this branch atf96d749.The controls say to read that second row as nothing. The AFTER half was
globally the faster of the two — but not uniformly, and the original text of
this paragraph overstated it (CORRECTED at review, and commit 894414a's
message carries the old claim): the sixteen non-elixir rows actually ranged
-0.25% to +4.10% (cs round_trip -0.12%, dart round_trip -0.25%, dart
write -0.02% came in DOWN; cpp round_trip +1.9%). The wider, mixed-sign
control band strengthens the conclusion rather than weakening it: elixir
write at +13.3% is well clear of that drift; elixir round_trip at +2.2% is
inside it, and net of cpp the blend moves 0.3%.
That is not evidence the lever does nothing. It is evidence this instrument
cannot resolve this question. One sweep is ONE sample per arm, and elixir's
round_trip rate varies far more between runs in a sitting than within one:
across the nine interleaved passes of the same main-branch code it ran
219,874 to 247,320, a 12.5% range, against the 0.55% spread inside a
single run. A 6% effect does not survive that. The sitting is labelled SHARED
in both preambles, which it was — a sibling session's C# round was live on the
same box.
So what a sweep actually recorded is 1413% -> 1409%. The interleaved
round_trip ratio would imply roughly 1330%, and I am not publishing that
number: no sweep measured it, and the §2.9 blend is a sweep statistic. The
honest position is that this lever's effect on the blend is smaller than the
sweep's own between-run variance, and a quiet box would be needed to put a
figure on it.
Why it is below the ~12% estimate — the mechanism, not a smoothing
The estimate came from #174's micro: one append of twelve segments at 73.3 ns
against twelve appends of one at 103.6, 1.41x. That is the cost of appends
in isolation. In the codec they are one term of write, and write is ~65% of
round_trip on this shape — so a 1.17x write is at most a 1.10x round_trip
before anything else is counted, and the blend statistic is round_trip.
The "slower derived read" is an arithmetic artifact, not a measurement
(REWRITTEN at review — the original paragraph here presented a 6-15% derived-
read slowdown as an unexplained reproducible effect). Derived read is
round_trip MINUS write, never a measured row (§2.9 says exactly this and why).
Any lever that improves write more than round_trip drives that derived number
backwards BY CONSTRUCTION, and its error band is enormous: read-time is ~35%
of round_trip here, so a 1% error in round_trip moves derived read ~2.9%, and
round_trip's own between-run range on unchanged code is 12.5% — a ±30%+ band
on the derived figure. The three "instruments" shared one round_trip number,
so they were never independent replicates. M is neutral on real read
traffic by construction: the read emission is byte-identical (all 1,232
lines diffed), and read-heavy workloads decode binaries that arrived from a
socket — already plain non-writable refc binaries, unreachable by anything a
writer-side lever changes. The one true residue: the round-trip loop pays an
emasculation copy when the reader touches the still-writable binary the same
process just built — a loop artifact, identical in size on both branches, and
the reason round_trip cannot capture the whole write gain.
Constraints held
actually carries an Elixir-only change is
test/elixir/main.exsbyte-comparing generated output against
testdata/wire/*.bin— green, andthe review proved it discriminating (a reversed barrier segment order goes
red on six checks). ("No wire golden moved" is true but near-vacuous for
this diff: goldens are produced by the C++ leg, which an Elixir emitter
change cannot touch.) corpus_id
6b213fbfa1a03a99.on.
mergeW's guard is unchanged, so a group is still at most 52 bits andthe scratch at most 7 + 52 = 59; a register is filled by exactly the spill
that used to append it, so it holds what the scratch held and nothing is
grown. Registers stay live to the barrier instead of dying at the append —
the deepest run in the tree spends 13 of the BEAM's 1024 (write_rigid_body, sc0..sc12 plus scratch — the review counted; the argument is unchanged).
mid-byte clause boundaries are exactly where a grouped write can disagree
with a grouped read, and this lever changes how many elements a write clause
takes. All nine legs byte-compare against the C++ pin.
— the new clause slots — and removes none; the display map resolves every
slot back to the element, so a message still reads
"e.delta is above the wire maximum". Every range, count, mask and length check is where it was.mix format's own shape for a longbitstring, so
--check-formattedpasses on the emitted text.make testgreen; regeneration zero-diff; every generatedmodule compiles under
--warnings-as-errors; shape-gate clean with no newledger entries;
mix format, modernize, gofmt andgo vetclean. The sevenSOURCE goldens the emitter moved are re-pinned in the lever's own commit.
serialize.elixiruntouched — generated Elixir has no runtime dependency.🤖 Generated with Claude Code