Skip to content

elixir #174: lever M — one binary construction per barrier - #207

Merged
gafferongames merged 3 commits into
mainfrom
elixir-lever-m
Sep 1, 2026
Merged

elixir #174: lever M — one binary construction per barrier#207
gafferongames merged 3 commits into
mainfrom
elixir-lever-m

Conversation

@gafferongames

@gafferongames gafferongames commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

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. 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 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 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 k — only the unroll cap of four does.

before after
static append sites in generated/bench/elixir 79 44
bench_mixed header 10 appends 1 (ten segments)
entity element 3 appends 1
stats clause 2 elements, 40 appends 4 elements, 20 appends

Measured

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/elixir under the pinned BEAM toolchain, the same
runner and the same corpus over two generated Bench.ex texts, max statistic,
median of per-pass ratios.

Ten passes, main vs M — M won 9 of 10 on write:

main lever M ratio
write 361,140 424,619 1.17x
round_trip 235,207 249,436 1.05x

Nine passes, three-way, separating M's two halves — C is the deferred
appends alone, with the clause k left where K had it:

B/A (full M) C/A (appends only) B/C (the clause widening)
write 1.168 1.099 1.070
round_trip 1.072 1.054 1.016

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_id 6b213fbfa1a03a99
on 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 at f96d749.

before after ratio
elixir write 383,329 434,318 1.133x
elixir round_trip 248,616 254,142 1.022x
blend (§2.9) 1413% 1409%

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

  • The Elixir wire is byte-identical to the C++ pin. The gate that
    actually carries an Elixir-only change is test/elixir/main.exs
    byte-comparing generated output against testdata/wire/*.bin — green, and
    the 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.
  • The fixnum envelope is untouched, which is what this whole round rests
    on. mergeW's guard is unchanged, so a group is still at most 52 bits and
    the 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).
  • Clauses.schema and Joins.schema pass, which is the point of them: their
    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.
  • Raise text and refusal semantics identical. The diff ADDS 34 raise sites
    — 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.
  • The barrier's construction is emitted in mix format's own shape for a long
    bitstring, so --check-formatted passes on the emitted text.
  • Nine-backend make test green; regeneration zero-diff; every generated
    module compiles under --warnings-as-errors; shape-gate clean with no new
    ledger entries; mix format, modernize, gofmt and go vet clean. The seven
    SOURCE goldens the emitter moved are re-pinned in the lever's own commit.
  • serialize.elixir untouched — generated Elixir has no runtime dependency.

🤖 Generated with Claude Code

…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>
@gafferongames
gafferongames marked this pull request as ready for review September 1, 2026 00:20
@gafferongames

Copy link
Copy Markdown
Contributor Author

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.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant