js: the write round — flat writes 1.89x, round_trip 1.47x; 461% -> 313% of generated C++ - #237
Merged
Conversation
… pair Profile (node --prof, bench_mixed --quick): 51% of ticks inside writeBenchMixedFlatProduction itself, 27% in V8 runtime calls it makes. The per-field merge carried two data-dependent branches (the s<32 lane split and the 64-bit flush) plus an s>0 guard; the single 32-bit staging word does one shift-or, one literal add, and one flush branch storing one word. Micro (interleaved, node 26, M2): 1.44x on a mixed-width group of ten. On the leg (quick, production): write 1.35 -> 1.49 M msg/s (+10%), round_trip 0.77 -> 0.82 (+6.5%). Wire bytes unchanged: an LSB-first packer into consecutive little-endian 32-bit words emits the identical byte stream; golden + 64-variant re-encode gates and test/js (both modes, degenerate corpus included) green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…sumes, nothing allocates
The profile's 27% V8-runtime bucket was BigInt: every BigInt '&', '>>',
asUintN-that-truncates and Number() on the write path allocates a BigInt
and calls the runtime, where a DataView setBigUint64 consumes the value
into plain memory without allocating. Micro (node 26, M2): 128-bit split
15.0 -> 84.3 M ops/s via two scratch stores + one shift (5.6x); sub-32
truncation 116.9 -> 718.9 M ops/s via scratch wrap (6.1x); the existing
<=64 scratch form measured already optimal (674 M ops/s) and keeps its
shape minus the redundant asUintN(64) wrap (ToBigUint64 IS that wrap;
for the >64 half, shift-then-wrap equals the asUintN(128) high half).
Reworked: emitWriteWideOffset takes the raw BigInt offset and routes all
four width classes through the scratch; flags <=32 wire mask through the
scratch instead of Number(asUintN); the >32-storage int32-range truncation
becomes SC.getInt32 (ToBigUint64 wrap + signed lane == asIntN(32) for
every BigInt). wideOffsetWidth deleted — nothing reduces ahead of the
store anymore. Checked-mode guards unchanged.
Leg (quick, production): write 1.49 -> 2.22 M msg/s (+49%), round_trip
0.82 -> 1.01 (+23%). Golden + 64-variant gates and test/js{,-ludicrous}
green in both modes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…s, one shift-or The read twin of the write-side lane discipline: the 96/128-bit assembly built its BigInt from four BigInt(v) constructions chained with three shift-ors (seven allocating steps); it now assembles each 64-bit half in the number domain through SC.setUint32 pairs and joins two getBigUint64 loads with a single shift-or. Micro: 12.5 -> 40.3 M ops/s on the 128-bit assembly (3.2x, node 26, M2). Leg (quick, production): round_trip 1.01 -> 1.06 M msg/s, derived read 1.86 -> 2.07; write untouched within spread. Gates green both modes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…nto one staged word The family's grouping lever (go #198's runs, cs #208's word codec) in the form this backend can hold: adjacent <=32-bit fields whose value expressions are pure (field loads, hoisted element refs, literals — never a mutable temp) pack into one 'v' with literal RELATIVE shifts, then merge once. Relative offsets inside a chunk are static even where the absolute cursor is dynamic, so the lever reaches the loop bodies carrying most of the corpus wire (the 80-element stat array drops from two merges per element to one; the entity body from ~14 to ~6). Chunks never cross a scope boundary or a statement-form merge — wire order is emission order, enforced by chunkFlush at every loop, branch, switch, align, and scratch-based field; a temp-based piece (the >32-storage int32-range 'n') flushes around itself so no piece outlives its inputs. Leg (quick, production): write 2.22 -> 2.48 M msg/s (+12%), round_trip 1.06 -> 1.11 (+5%). Wire identical: golden + 64-variant gates, test/js{,-ludicrous} both modes, degenerate corpus — green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Measured first as a hand-patched ceiling on the generated leg (+3.6% write), then landed in the emitter: the byte-copy loop for string(N)/ bytes(N) takes 4 bytes per 32-bit merge with a byte-at-a-time tail. The counter lives in a brace scope so two byte fields can share a nesting depth. Leg (quick, production): write 2.48 -> 2.55 M msg/s, round_trip 1.11 -> 1.12. Gates green both modes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Source goldens only — testdata/wire is untouched by this round (the stop-the-line invariant), and the full nine-language make test is green against the unchanged wire pins. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
gafferongames
marked this pull request as ready for review
September 1, 2026 03:19
gafferongames
added a commit
that referenced
this pull request
Sep 1, 2026
…k-bytes path; 363% -> 221% of generated C++ (#238) * dart: chunked write merges — 64-bit lanes, single-merge wide fields Adjacent constant-width pure-expression fields pack into one staged word with literal relative shifts (the family lever, #198/#208/#237), capped at Dart's native 64-bit lane where the js number domain held it to 32. Wide 33..64-bit fields merge once instead of twice — their wire bits are contiguous, so the bytes are identical by the concatenation argument. Leg (quick, arm64 M2): write 1.894 -> 2.380 M msg/s (1.26x), round_trip 0.974 -> 1.088 (1.12x). Golden gate + 64-variant re-encode green; dart conformance suites green with asserts on. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * dart: bulk-bytes write path — the #165 lead String/bytes bodies and bare [N]uint8 arrays stop paying one merge per byte: static runs of 8 or fewer bytes queue as chunk pieces (joining neighboring fields in one merge), longer and dynamic runs take 4 bytes per merge with a byte tail. No alignment requirement: LSB-first merging of the concatenation equals merging the bytes in sequence at any bit position, so the fix reaches the unaligned loadout too. Micro (AOT, M2): 4-byte grouping 2.33x, per-byte baseline 1.00x. Leg (quick): write 2.380 -> 2.499 M msg/s (+5.0%), round_trip 1.088 -> 1.104 (+1.4%) — bulk is only 3.65% of bench_mixed's wire by construction; the lever is for the real packets the issue names. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * dart: chunked read windows — one load and one tail branch per run, not per field The reader kept reloading its 64-bit window at every field. A load now folds the byte-interior shift in and guarantees 57 valid bits (in the tail arm, every bounds-proven remaining bit), so consecutive constant-width fields extract at literal relative shifts from one window through a compile-time ledger. Wide 33..57-bit fields extract in one masked read — their groups are contiguous on the wire. Short bare byte arrays unroll into the same windows (#165's read twin). The ledger invalidates at every dynamic bitsRead move and scope boundary. Micro (AOT, M2): chunked window 1.35x over per-field reload. Leg (quick): round_trip 1.104 -> 1.367 M msg/s (+23.8%), write flat; derived read 2.01 -> 3.03 M msg/s. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * dart: grouped counted loops — k elements share one lane per iteration A counted array of static-width elements unrolls k = lane/elemBits elements per iteration (64-bit chunk lane on the write side, 57-bit window on the read side), with a remainder loop. The chunk accumulator and window ledger span the unrolled elements, so short elements merge together and extract from one load — bench_mixed's 80-element stat array drops from one merge and one window load per element to one per three. Hoisted element refs get unique names (e0, e0g1, e0g2) so the unroll shares one scope; correctness rides the same ledger/flush machinery as straight-line code. Bounded first by a hand-patched generated leg (+10% round_trip on the read side alone). Leg (quick): write 2.493 -> 2.856 M msg/s (+14.6%), round_trip 1.367 -> 1.592 (+16.5%). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * dart: re-pin source goldens for the chunked emission Wire goldens untouched — testdata/wire byte pins hold in every leg; the re-pin is the SOURCE form only (SPEC §3.1's deliberate-change path). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * dart: range-over-int in the emitter's unroll loops (modernize) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * bench/results: the dart round's certification pair before 1,900,191/972,818 (reproducing the standing ledger row within 0.6%), after 2,857,586/1,591,336; spreads 0.07-2.28%; one sitting, corpus 6b213fbfa1a03a99. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: Rowan Claude <rowan@mas-bandwidth.com>
This was referenced Sep 1, 2026
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.
Laggard round for generated JavaScript (#173): the flat tier's write side dragged the blended row — reads were already native-class. This is the write-side profile and the levers it justified.
Headline (
--only js --quick, both halves in one sitting minutes apart,bench_mixed, familygen, codecflat,checks=contract, corpus6b213fbfa1a03a99, max rates per §2.2, spreads ≤ 0.9%):Against the arm64 sitting-2 certification reference at the same
corpus_id(cpp round_trip max 3,512,100 — the LOCKed reference, untouched by this round): 459.2% → 313.1% on this pair's own before leg (the ledger's standing 461% reproduced within 0.3%) of generated C++ on the canonical round_trip blend; on write 553% → 292%. Derived read moved 1.76 → 2.00 M msg/s as a side effect of the wide-lane lever — the read side was not traded away. The measurement CSVs live outsidebench/results/(scratch, per the round's rules); the numbers above are their rows verbatim.1. MEASURE FIRST — where write time actually was
node --profover the conformant quick leg (BENCH-STANDARD §2.8; node 26.7, M2, production mode):writeBenchMixedFlatProductionitself — straight-line merge arithmetic, no calls;The per-field merge carried two data-dependent branches (a
s < 32lane split and the 64-bit flush) plus ans > 0guard; every 128-bit field paid ~7 allocating BigInt steps; every&/>>/Number()/truncating-asUintNBigInt op allocates and calls the runtime.The micro that ranked the primitives (interleaved arms, node 26, M2): DataView BigInt scratch stores consume a BigInt into plain memory without allocating —
setBigUint64+2×getUint32runs 674 M ops/s where the& 0xffffffffn/>> 32n/Number()chain runs 29 M. That asymmetry is the whole write-side BigInt story.2. THE LEVERS, each landed as one commit with its paired numbers
SC.setBigUint64(ToBigUint64 IS the asUintN(64) wrap; shift-then-wrap equals the asUintN(128) high half, so no reduction ever allocates ahead of the store); flags ≤32 and the >32-storage int32-range truncation (SC.getInt32≡asIntN(32)) ride the same scratchBigInt(v)constructions chained through three shift-orsRefused by measurement
Named follow-on (land-and-expand, not this round)
Read-side window chunking: the same relative-shift argument applies to reads — one 64-bit window load can feed several extractions where today every field reloads two words. The issue rules reads out of this round ("the read side needs nothing"); it is the next pass if the blend is chased further.
3. Runtime-side lever, reported not implemented
serialize.js's ownBitWriter.writeBits(src/bitpacker.js) carries the exact two-lane 64-bit staging form this round retired in the flat tier — L1's micro (1.44x on a mixed-width group) prices a single-word 32-bit staging rewrite there. It would move the jsbitsfamily rows (the runtime bitpacker), not the flat gen rows, and it touches the four fused copies the file itself warns about plus flush/reset invariants — a serialize.js PR, not a schema one.4. Discipline
make testgreen;testdata/wireuntouched (the re-pin commit touches SOURCE goldens only); the bench leg's §1.5 golden + 64-variant decode→re-encode gates pass atcorpus_id 6b213fbfa1a03a99; degenerate corpus (The bench corpus cannot see fixed scalar arrays, bare float units, or run-cap-split nested structs #203) included, both NODE_ENV modes.test/jsproduces 100 failure lines and exits non-zero; restored, green, regenerated tree byte-identical.go vetclean, modernize clean, shape-gate clean, generated tree current.bench/LOCKuntouched: nothing underinternal/codegen/{c,cpp}orgenerated/{c,cpp,c-ludicrous}moved.🤖 Generated with Claude Code
Certification pair committed under
bench/results/2026-09-01-jswrites-{before,after}-quick-arm64-macbook.csv— spreads 0.22–0.82%, recomputable forever. Adversarially reviewed: differential oracle (five seeds × ~600 rounds × four corpora, checked+production modes, cross read-back) zero divergence; every dropped-reduction equivalence claim verified with failed falsifications; negative control reproduced. Verdict: MERGE.