elixir #174: the aggressive round — 3743% to 1457% of generated C/C++ - #202
Conversation
The profile, and the ranked lever list — posted before the work (#174's method)1. Where the time actually goesInstrument: wall-clock A/B on the canonical shape, The write path is twice the read path. eprof agrees on the ordering The mechanism, named. Not a routing question. Generated Elixir has no runtime dependency; 2. The levers, measured in isolation before rankingMicro-benchmarks reproducing the shipped loop shapes exactly (the shipped
Two results that decide the design:
3. Ranked by win / effort
Expected combined: write ~2.2x, read ~2.1x, blended |
2257b81 to
11021b8
Compare
The round's result: 3624% → 1451% of generated C/C++, and the floor namedFour more levers since the ranked list above — D and J from it, K and L The certification numbers
Elixir's own rates: write 133,697 → 365,823 msg/s (2.74x), round_trip Whole round, against the pre-round baseline, on the scratch A/B (the
The four leversD — the static offsets the generator already knew. A write surface MEASURED: at or below the noise floor. ~2% on write, and the honest J — a scalar array element sized its own read window. The run fuser is K — one append for a whole clause of elements. This is where the write The list clause now takes k elements, k being the most whole elements whose MEASURED: write 3.097 → 2.65 us/op, 1.17x. round_trip 1.11x. L — one window decode for a whole clause of elements. K's other half. MEASURED: read 2.089 → 1.838 us/op, 1.14x. round_trip 1.07x. Both needed the element emitters to take the variable they bind (so a clause The floor, with its mechanismAt 1451% the port is 14.5x the fastest language — inside the five-to-
Two things follow, and they are the floor. 1. The append still dominates, and the fixnum boundary caps how much of it 2. Under the serialization there is a per-element cost no codec can Ranked next, named and not builtM — one binary construction per barrier, not one per flush. The one N — a NIF backend. #174's reserved second tier, and after this round it Ruled out and measured, unchanged from the list above: F is superseded by K Constraints heldWire bytes never moved: no file under The micro-benchmarks quoted here live in scratch, not in the repo — #200's 🤖 Generated with Claude Code |
11021b8 to
2b9e085
Compare
Wall-clock decomposition of generated Elixir on the canonical shape
(bench_mixed, 438 bytes: 8 entities x 135 bits, 80 stats x 18 bits,
4 loadout bytes), M2 Air, OTP 29.0.5 / Elixir 1.20.4, 100k ops per
measurement, the same instances the bench rotates:
write 7.344 us/op
read 3.561 us/op (round_trip 10.748)
write splits:
stats loop, 80 elements 2.632 us 32.9 ns/stat (36%)
entities, 8 elements 2.970 us 371.5 ns/ent (40%)
header + footer + loadout 1.742 us (24%)
The write path is 2x the read path, and 325 binary appends per message
is what it is made of: the emitter's mergeW flushes every whole byte
after EVERY field, so one bs_append BIF call rides each of the 131
static field sites plus 8x14 + 80x2 + 4x1 loop sites.
eprof agrees on the ordering (stats 38.7%, entities 23.7% of write) and
is not quoted for magnitudes: 162000 traced calls into the stats loop
inflate exactly the function the wall clock says is hot.
Nothing measured here lands in serialize.elixir. The generated Elixir
codec has no runtime dependency at all, so every lever below is a
schema-level emitter change and #170's routing question does not arise.
The emitter knew every field width statically and threw the knowledge
away: mergeW flushed the scratch's whole bytes after EVERY field, so a
438-byte bench_mixed message cost 325 bs_append BIF calls.
mergeW now carries a GROUP. It merges into the scratch and flushes only
when the next field would pass the budget, and flushW closes the group at
every barrier that observes data or scratch_bits — the write function's
tail, an align, the bytes of a string, a loop helper's call and its own
element tail, and the joins of a branch or a union case. flushW is a
no-op when no group is open, which is what makes a barrier free where one
is already closed.
The budget is 52 bits and the number is not a taste. The BEAM's fixnum is
60-bit signed, so an intermediate at or above 2^59 costs a heap bignum; a
flush leaves at most 7 bits behind, so 7 + 52 = 59 is the whole envelope.
Measured on the read side of the same shape, a 72-bit window is SLOWER
than the shipped 40-bit one (29.4 vs 27.6 ns/element) — boxing costs more
than the flush it would save, so the group stops at the boundary.
MEASURED, wall clock, canonical shape, 100k ops (before -> after):
write 7.344 -> 5.208 us/op 1.41x
stats loop 32.9 -> 22.3 ns/stat
entities 371.5 -> 223.2 ns/entity
round_trip 10.748 -> 8.886 us/op
read 3.561 -> 3.561 us/op (untouched, as expected)
bench --quick: write 0.13 -> 0.18 M msg/s, round_trip 0.09 -> 0.12
Static append sites in generated/bench/elixir: 131 -> 65; per entity
14 -> 3, per stat 2 -> 1; ~325 -> ~128 appends per message.
Wire bytes unmoved: corpus_id 6b213fbfa1a03a99, the full variant
round-trip gate green, test/elixir and test/elixir-ludicrous OK,
mix format --check-formatted clean.
rd/3 opened a fresh match context for EVERY field: 14 per entity, 2 per
stat. The generator knows every width statically, so readR now reads a
GROUP into rv once and cuts each field out with a static shift and mask.
The fused static run's own length sizes the group, so a short run keeps
the cheap 40-bit window and a long one takes rdw's 56-bit window.
Two windows, and the second one's width is the same fixnum argument the
write group's budget rests on. A 56-bit window less the 7-bit worst-case
offset is a 49-bit group and stays under 2^59; a 64-bit window would box.
Measured on this shape a 72-bit window is SLOWER than the shipped 40-bit
one it would replace — 29.4 vs 27.6 ns/element — so 49 is the ceiling and
not an arbitrary stopping point.
Reading a group wider than the fields it feeds is safe by construction:
rd and rdw never raise (the tail falls back to the bytes that exist), and
bits past the bounds-checked run are discarded, never observed. Every
bounds check, range check, constant check and refusal is where it was.
rdBreak closes the group at every barrier — the read surface, a loop
helper's entry and every call to one, an align, the bytes of a string,
and the arms of a branch or a union case.
MEASURED, wall clock, canonical shape, 100k ops (after lever A -> after C):
read 3.561 -> 2.384 us/op 1.49x
stats 27.6 -> ~15 ns/stat (rd calls 2/elem -> 1)
entities (rdw calls 14/elem -> 4)
round_trip 8.886 -> 8.126 us/op
write 5.208 -> 5.208 us/op (untouched, as expected)
bench --quick: round_trip 0.12 -> 0.13 M msg/s
Wire bytes unmoved: corpus_id 6b213fbfa1a03a99, the full variant
round-trip gate green, test/elixir and test/elixir-ludicrous OK,
mix format --check-formatted clean.
Elixir's `.` on a struct is a get_map_elements of its own with its own
raise branch. The writer spent one per field: 14 for an entity, 2 for a
stat, ~30 at the top level. A scope now reads its fields ONCE —
`%{stat_id: e_stat_id, delta: e_delta} = e` — and every reference to
those fields is a local from there on.
Scope, precisely: a scope binds the fields it reads UNCONDITIONALLY at
its own level — its field items and the conditions of its branches. A
branch arm's fields are NOT bound at the enclosing level; the arm is its
own scope and binds them when it is taken, so a value the wire never asks
for is still never demanded of the caller. Below two fields the pattern
would not pay for itself and none is emitted.
Refusals are unchanged. A struct always carries every key, so the bound
locals are exactly as unconditional as the dotted accesses they replace,
and every range, count, mask and length check is where it was. The one
delta, named: a MAP missing a key now raises MatchError where it raised
KeyError — a raise either way, on exactly the same inputs, never a wrong
answer. The raise TEXT is unchanged: g.dsp resolves a local back to the
dotted access it stands for, so the message still reads "e.delta is above
the wire maximum" and never names a local the caller never wrote.
MEASURED, wall clock, canonical shape, 100k ops (after lever C -> after B):
write 5.208 -> 3.202 us/op 1.63x
stats loop 23.4 -> 15.4 ns/stat
entities 212.6 -> 96.4 ns/entity
round_trip 8.126 -> 5.466 us/op
read 2.384 -> 2.276 us/op (the read side binds locals already)
bench --quick: write 0.18 -> 0.31 M msg/s, round_trip 0.13 -> 0.18
Cumulative over A + C + B, against the round's baseline:
write 7.344 -> 3.202 us/op 2.29x
read 3.561 -> 2.276 us/op 1.56x
round_trip 10.748 -> 5.466 us/op 1.97x
Wire bytes unmoved: corpus_id 6b213fbfa1a03a99, the full variant
round-trip gate green, test/elixir and test/elixir-ludicrous OK,
mix format --check-formatted clean.
Three changes to the compressed-float helpers, all of them removing work that was provably redundant: fr/1 lets the REFUSAL be the test. A float segment does not match a non-finite pattern, so the finite path is one construction and one match and never touches the exponent field; the second clause reads the sign of exactly the patterns the first refused. NaN still maps by sign, as it did. cf_quantize takes miv32 — the float32 of the step count — as a generation-time literal instead of computing fr(miv * 1.0) on every call. It is a declaration constant; folding it removes one of the six float32 steps. cf_decode takes it directly in place of the integer count, which was only ever used to compute the same rounding. trunc(Float.floor(x)) becomes floor(x): one BIF returning an integer instead of a float floor and a truncation. The argument is finite by construction (normalized is clamped to [0, 1] and miv32 is finite), so the paths differ only in the class of an unreachable raise. MEASURED, wall clock, canonical shape, 100k ops, and reported honestly: write 3.202 -> 3.18 us/op AT OR BELOW the noise floor read 2.276 -> 2.19 us/op ~3% round_trip 5.466 -> 5.30 us/op The isolated micro said more (cf_quantize 203.6 -> 135.2 ns/call), and the micro was wrong to say it: it passed fr as a closure, so every fr call it removed was an inflated indirect call rather than the direct local call the generated module makes. The in-situ number is the ruling. The lever stays because it costs nothing and removes real work; it is recorded as small, not as what the micro promised. Wire bytes unmoved: corpus_id 6b213fbfa1a03a99, the full variant round-trip gate green, test/elixir and test/elixir-ludicrous OK, mix format --check-formatted clean.
Deliberate emitter change, and only the SOURCE goldens move: no file under testdata/wire is touched by this commit or by any of the four levers, and the full `make test` chain — every language's conformance suite, the wire goldens, the fuzzers, the format refuser — is green.
The group model carried its widths statically and then computed with them
at runtime anyway: every merge shifted by the scratch_bits VARIABLE and
added to it, and every flush divided that variable by eight to size a
binary segment whose width was therefore dynamic.
A write function starts at a known empty scratch and stays known until the
message's own data decides a length. Through that whole region the emitter
now tracks the offset itself, so:
- a merge is one statement, `scratch = scratch ||| v <<< 16`, with a
literal shift and no scratch_bits arithmetic at all; the first merge of
an empty group is a bare bind, `scratch = v`
- a flush is `data = <<data::binary, scratch::little-size(4)-unit(8)>>`
with a LITERAL segment width — the form the BEAM's binary construction
is built for — plus a literal shift, in place of four statements
- an align that lands on a boundary emits nothing, and one that does not
appends the residual byte unconditionally instead of testing for it
- the function's tail is `data` or `<<data::binary, scratch>>` outright,
never `if scratch_bits != 0`
- scratch_bits is not even bound where the surface never needs it
Staticness is a property the emitter EARNS and gives up honestly. It is
given up where a loop helper is called (how many elements rode is the
message's business) and where a branch's arms end on offsets that disagree;
in that case each arm publishes the offset it reached and the emitter goes
back to maintaining the variable. It is REGAINED at every align, which
lands the position on a byte whatever the data did. Where every arm of a
branch or a union case does agree, the offset stays static past the join
and scratch_bits does not ride the join's tuple at all.
Behaviour is unchanged: the same statements in the same order over the same
values, with the arithmetic the generator can do moved to generation time.
Every range, count, mask and length check is where it was, and every raise
carries the text it carried.
Wire bytes unmoved: no file under testdata/wire is touched, corpus_id
6b213fbfa1a03a99, the full variant round-trip gate green, test/elixir and
test/elixir-ludicrous OK, mix format --check-formatted clean, and the whole
nine-backend `make test` chain green. Only the four SOURCE goldens move,
re-pinned here with the emitter that moved them.
MEASURED: in the sitting's A/B, recorded on the PR with the round's table.
Lever C sizes each window decode by the fused static run it sits in, and the run fuser is reached through emitReadItems. A scalar array element is not: readHelper hands it straight to the scalar read, so the run was zero — unknown — and readR fell back to its widest window. A one-byte element was opening the 56-bit window to take eight bits. That is not merely a wider mask. rdw needs seven bytes ahead of the position to match its window and rd needs five, so the wide window drops into the :binary.decode_unsigned tail fallback two bytes sooner — and a scalar array is very often the LAST thing in a message, which is exactly where that boundary lies. A scalar element's own width is the run, so readHelper sets it. The loadout loop now reads `rd(data, bits_read, 8)` where it read `rdw(data, bits_read, 49)`. Nothing else moves: a struct or union element still goes through emitReadItems and fuses its own runs, and readR's choice of window was already free to be any width that covers the field. Wire bytes unmoved: no file under testdata/wire is touched, corpus_id 6b213fbfa1a03a99, the full variant round-trip gate green, test/elixir and test/elixir-ludicrous OK, mix format --check-formatted clean, the whole nine-backend `make test` chain green, and the two source goldens the emitter moved re-pinned here. MEASURED: in the sitting's A/B, recorded on the PR with the round's table.
…each The write loop appended once per ELEMENT: 80 bs_append calls for the stats array of one bench_mixed message, 4 for the loadout. Lever A closed the group across the fields of an element; it could not close it across the call boundary between elements, because a helper is entered at an offset the caller's element count decides. Measured in isolation, that boundary is where the write path's cost is. Twelve appends of one 32-bit segment each cost 103.6 ns; the same 48 bytes in three appends of four segments cost 79.0, and in one append of twelve, 73.3. The same twelve appends with a DYNAMIC segment width cost 103.7 — identical. The append is the expense; the arithmetic around it is not. So a clause takes SEVERAL elements off the list. k is chosen by the group budget the fixnum boundary already fixed — the most whole elements whose widths fit in 52 bits, capped at four so one array field cannot cost unbounded generated code — and the k element bodies merge into one group and flush once. The single-element clause behind the wide one is the remainder, so a list length never has to divide anything, and an element whose width the wire decides keeps one clause per element as before. stats (18 bits): 2 elements per clause, 80 appends -> 40 loadout (8 bits): 4 elements per clause, 4 appends -> 1 Nothing about an element's emission changes. The clause names its slots e1 and e2 where it used to have just e, and the raise TEXT is held: the display map resolves every slot back to "e", so a message still reads "e.delta is above the wire maximum" and never names a slot the caller never wrote. Every range, count, mask and length check is where it was. MEASURED, wall clock, the canonical shape from the committed variant corpus, 64 rotating instances, 150k ops, 4 runs x 4 interleaved passes, best per pass, median of passes: write 3.097 -> 2.65 us/op 1.17x round_trip 5.268 -> 4.74 us/op 1.11x read 2.132 -> 2.09 us/op (untouched, as expected) Wire bytes unmoved: no file under testdata/wire is touched, corpus_id 6b213fbfa1a03a99, the full variant round-trip gate green, test/elixir and test/elixir-ludicrous OK, every generated module compiles without a warning, mix format --check-formatted clean, the nine-backend `make test` chain green, and the source goldens the emitter moved re-pinned here.
The read loop's other half of lever K. Lever C reads a GROUP into one window and cuts each field out of it with a static shift and mask, but a loop helper is a function boundary, so a group could never span two elements: 80 window decodes for the stats array, 4 for the loadout. A read clause now decodes k elements under ONE window. k is chosen by the wide window's usable width — the most whole elements whose widths fit in 49 bits, which is the same fixnum boundary the write budget rests on, capped at four — and the clause carries a guard on the remaining count, with the single-element clause behind it as the remainder. Two things had to give way for a window to span elements. The element emitters now take the variable they bind, so a clause can hold e1 and e2 without either shadowing the other. And the run fuser no longer overwrites an OUTER run: a named element's own scope would otherwise size the window to one element and undo the clause. Where no outer run is open the fuser behaves exactly as it did. stats (18 bits): 2 elements per rdw window of 36 bits, 80 decodes -> 40 loadout (8 bits): 4 elements per rd window of 32 bits, 4 decodes -> 1 Reading a window wider than one element is safe on exactly the grounds lever C established: rd and rdw never raise, the tail falls back to the bytes that exist, and bits past the bounds-checked run are discarded and never observed. The bounds check itself is unchanged — the call site proved count * elem for a counted array, and an unbounded run still checks its own span before it reads it. MEASURED, wall clock, the canonical shape from the committed variant corpus, 64 rotating instances, 150k ops, 4 runs x 4 interleaved passes, best per pass, median of passes, against lever K: read 2.089 -> 1.838 us/op 1.14x round_trip 4.736 -> 4.409 us/op 1.07x write 2.703 -> 2.691 us/op (untouched, as expected) Wire bytes unmoved: no file under testdata/wire is touched, corpus_id 6b213fbfa1a03a99, the full variant round-trip gate green, test/elixir and test/elixir-ludicrous OK, every generated module compiles without a warning, mix format --check-formatted clean, the nine-backend `make test` chain green, and the source goldens the emitter moved re-pinned here.
Four findings, all of them this branch's own, none of them a behaviour change: the two scope-binding map copies lever B introduced become maps.Copy, and the two unroll caps levers K and L introduced become min. The generator emits identical bytes for every schema in the tree. CI's lint job runs modernize at @latest, so a check that ships tomorrow lands on the next branch the same way; this is the branch paying for its own four.
#198 landed `examples/Degenerate.schema` and pinned its Elixir source golden with the pre-round emitter, so this branch merging as-is turned main red: `TestGoldenSourceElixir` refused `testdata/golden/elixir/Degenerate.ex`. The re-pin is a SOURCE re-pin only. `make update-goldens` rewrote every wire golden under `SCHEMA_UPDATE_WIRE_GOLDENS=1` and every one came back byte-identical, `testdata/wire/` has no diff, and the nine-backend chain byte-compares `degenerate.bin` against the C++ pin in all nine legs and is green. The new text is the round's own shapes: one map destructure per scope (B) and literal-width flushes with `scratch_bits` gone (D).
81ef716 to
6cf2c19
Compare
… cannot reach Degenerate.schema's standing property is that every type in it is a whole number of bytes. That is load bearing for what it catches and it is also a ceiling: no clause boundary inside it ever lands mid-byte, so an emitter that groups array elements picks the same group size on the write and the read side of every type in the file. The Elixir round's write clause has a 52-bit budget and its read clause a 49-bit window, so on Degenerate the two always agree and the disagreement is untested. Clauses.schema picks element widths where they do not. At 13 bits the write clause takes four elements (52, the whole budget) and the read clause three (39, inside the window); at 17 it is three against two; at 26, two against one. Counts run 0, below a clause, exactly a clause, one past it, and the bound, so the remainder path is entered from every phase. It also carries a fixed mid-byte array, grouping across a nested struct boundary, a union of empty arms behind a tag, and string/bytes at zero, partial and full length behind a 5-bit lead so the align inside them is a real barrier. Joins.schema does the same to the static-offset state machine: arms that agree and disagree on width, a branch with no else, a branch inside a branch, an align that regains staticness on one path only, an array that gives it up on one path only, unions of unequal arms at mid-byte offsets, and a long static run after an align. Unlike Degenerate these shapes are NOT byte-aligned, so one shared stream would not equal a concatenation of the shapes written alone — and the Elixir emitter returns each message as its own binary from bit zero, so it cannot write a shared stream at all. Every shape is therefore written to its own stream and flushed, and the golden is those concatenated. Every leg can reproduce that, and each shape's bytes stay individually attributable. C++ pins, Go and Elixir byte-compare; the remaining six legs follow. Adding units to the corpus moves the example unit's protocol id, as #198 did when it added Degenerate. No existing wire golden moved.
All nine legs now hold the two units to the C++ pin. The corpus README gains a row for each, stating what each reaches that Degenerate cannot.
The published figures were unverifiable: `git log origin/main..HEAD -- bench/results/` was empty, so no re-runner could check them. #198, #199 and #204 all committed their before/after data this week; this matches that. Both halves are a fresh paired A/B taken in one sitting on the M2 Air after the rebase, three minutes apart, corpus_id 6b213fbfa1a03a99 on every row. The BEFORE half stamps main at 52691a0 and the AFTER half elixir-swizzle at fef86ed — the last CODE commit on the branch, so both stamps resolve on origin and reconstruct the tree that was measured. Elixir, bench_mixed, family gen: write 135,696 -> 378,044 msg/s (2.79x; spread 2.08% -> 0.22%) round_trip 95,413 -> 239,723 msg/s (2.51x; spread 0.46% -> 0.38%) As a share of generated C++ (§2.9, max rates): 3743% -> 1457%. Both elixir rows are far inside §2.3's 15% noise gate, so neither is a row bench/tools/relative.go would exclude.
The adversarial review's five, answered — and its corpora adoptedThe review returned BLOCK on five items, none touching the wire, which it 1 — REBASE (hard blocker). Fixed. The branch's merge-base was 2 — COMMIT THE CSVs. Fixed, by re-measuring. The original sweep's CSVs 3 — THE HEADLINE. Fixed, and the review was right about the mechanism. 4 — "none of them moves more than 2%". Corrected, and it was false in my On direction: the five tight controls all moved the same way (+0.3% to 5 — THE #174 FRAMING. Fixed. The body now says what the correction on The corpora, promotedThe review's The gap is real and structural, and the generated code confirms it directly. One correction to the review's framing, found while adopting them: the Verification
🤖 Generated with Claude Code |
Main at d00cf49, all of the night's emitter work landed (#198 go, #202 elixir), box idle, nine legs, corpus_id 6b213fbfa1a03a99. c 98 / cpp 100 / rust 154 / java 157 / go 239 / cs 361 / dart 381 / js 486 / elixir 1498 This is ledger point one for #194: the first sitting where the canonical shape, the data-driven harness, and the night's optimizations are all on main together, measured with nothing else running. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* elixir: lever M — one binary construction per barrier, several registers 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. * bench: commit both halves of the sitting lever M was measured in 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. * elixir emitter: segW does not survive a join — the guard the #207 review 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> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Issue #174. The Elixir position — thousands of percent of generated C/C++ on
the §2.9 blend — worked with the method the owner set: profile first,
enumerate every lever, rank by win/effort, take them one commit and one
measurement at a time, and record the ones that fail as results.
3743% → 1457% of generated C/C++. Write 2.79x, round_trip 2.51x, both
halves measured in one sitting after the rebase and both committed:
bench/results/2026-08-31-elixirswizzle-{before,after}-quick-arm64-macbook.csv.The per-lever figures are each commit's own measurement, recorded as the
lever landed. The certification pair below is the committed sweep.
The certification pair, and what the controls actually did
bench/run.sh --quick, both halves three minutes apart on the same fanlessM2 Air, corpus_id
6b213fbfa1a03a99on every row. BEFORE stamps main at52691a0; AFTER stamps this branch atfef86ed— the last code commit, soboth stamps resolve on origin and reconstruct the tree that was measured.
Elixir's own rates: write 135,696 → 378,044 msg/s, round_trip 95,413 →
239,723 msg/s. Both elixir rows sit far inside §2.3's 15% noise gate
(spreads 0.22% and 0.38%), so neither is a row
bench/tools/relative.gowould exclude.
Five of the seven non-cpp controls hold inside 1.3%. Two do not, and
saying otherwise would be false: js +3.05% and java −9.28%, the two JIT
legs, and both had the noisier half in BEFORE (js spread 3.3%, java 1.9%,
against 0.4% and 0.2% after). Read them as JIT variance, not as signal.
The five tight controls all moved the SAME way, +0.3% to +1.3%, which means
cpp was marginally faster in the AFTER sitting. That drift runs against
this headline, not with it: a relatively faster cpp inflates every other
language's percentage, so 1457% is if anything slightly high and the
improvement slightly understated. The result is conservative.
An earlier revision of this PR published 3624% → 1451% from an
uncommitted sweep. That pair is withdrawn. Its elixir round_trip row had
come in at 15.3% spread against the 15.0%
spreadNoisythreshold inbench/tools/relative.go:86— the repo's own tool would have excluded it,and because the headline uses the max statistic, a noisier row carries a
luckier max. It was also the most flattering of the three figures then in
hand (1491%, 1456%, 1451%). The pair above replaces it with clean rows and
committed data; it moves the substance not at all.
What lever D does and does not retire (issue #174)
The issue proposed one binary construction per word with pre-swizzled
segments. That is two claims, and only one of them is dead.
retired. Lever D built it exactly as specified and measured ~0. The micro
isolating it settles why: twelve appends of one 32-bit segment cost 103.6
ns, and the same twelve appends with a DYNAMIC segment width cost 103.7 —
identical. The literal widths were never the term that mattered.
than one append each — is live, and it is lever M. Its micro is direct
evidence for the issue's own mechanism: one append of twelve segments is
73.3 ns against 103.6 for twelve appends of one, 1.41x. M is
prototyped, byte-identical in output, and measured at 1.14x on the stats
loop.
So #174's hypothesis is not retired. What the round established is that
append COUNT dominates — the same insight the hypothesis was reaching for by
a different route. (This matches the correction already posted on #174.)
The floor, with its mechanism
At 1457% the port is 14.6x the fastest language — inside the five-to-fifteen
band #174 asked to have published rather than smoothed, and still ~3x short
of the generated-JS tier. The shipped stats clause, decomposed a layer at a
time: append 7.96 of 13.19 ns/stat, merge 2.03, contract checks 0.76, and
2.44 ns/stat just walking a list of maps and reading two fields before any
bit is packed. The BEAM's 60-bit fixnum caps a scratch register at 59 bits,
which is why the write group budget is 52 and the read window 49, and which
caps how much of the append cost ANY grouping can remove.
Named and not built: lever M (above), the only remaining pure-BEAM move,
~12% on the blend; and a NIF backend, which is the owner's decision and
not an optimization.
New: the corpus gap this round exposed
Degenerate.schema(#203) has a standing property — every type in it is awhole number of bytes. That is load bearing for what it catches and it is
also a ceiling: no clause boundary inside it ever lands mid-byte, so this
round's grouped write clause (52-bit budget) and grouped read clause (49-bit
window) always pick the same element count there. The disagreement was
untested by construction.
Two new corpus units close it, adopted from an adversarial review of this PR:
Clauses.schema— element widths where the two sides choose differentk. At 13 bits the write clause takes four elements (52, the whole budget)
and the read clause three (39, inside the window); at 17 it is three
against two; at 26, two against one. Counts run 0, below a clause, exactly
a clause, one past it, and the bound. Plus a fixed mid-byte array, grouping
across a nested struct boundary, a union of empty arms behind a tag, and
string/bytesat zero, partial and full length behind a 5-bit lead sothe align inside them is a real barrier.
Joins.schema— the same aimed at the static-offset state machine:arms that agree and disagree on width, a branch with no
else, a branchinside a branch, an align that regains staticness on one path only, an
array that gives it up on one path only, unions of unequal arms at mid-byte
offsets, and a long static run after an align.
Because these shapes are NOT byte-aligned, one shared stream would not equal
a concatenation of the shapes written alone — and the Elixir emitter returns
each message as its own binary from bit zero, so it cannot write a shared
stream at all. Each shape is therefore written to its own stream and flushed,
and the golden is those concatenated. All nine legs byte-compare against
the C++ pin, and each shape is held to its own declared bit width so a
doubled or dropped clause is named rather than smeared into one total.
Constraints held
git diff origin/main -- testdata/wire/shows only the two NEW corpus goldens,
clauses.binandjoins.bin.make update-goldensrewrote every wire golden underSCHEMA_UPDATE_WIRE_GOLDENS=1and every pre-existing one came backbyte-identical.
6b213fbfa1a03a99unchanged;bench/otherwise untouched.examples/moves theexampleunit's protocol id intestdata/golden/id.txt, exactly as go: the flat word codec — 712% to 248% of generated C/C++ #198 did when it addedDegenerate.schema.testdata/golden/elixir/Degenerate.exwiththe pre-round emitter, so merging unrebased would have turned main red on
TestGoldenSourceElixir; that source golden is re-pinned here.make testgreen;regeneration zero-diff (
make generated-current); shape-gate clean with nonew ledger entries;
mix format,modernize,gofmtandgo vetclean.serialize.elixiris untouched and could not be otherwise — generatedElixir has no runtime dependency at all, so the whole seam the owner's
grant opened is on the emitter side of it.
🤖 Generated with Claude Code