C++ emitter: bulk-bytes path for statically byte-aligned [N]uint8 arrays - #7
Merged
Conversation
Profile conviction (serialize PR #27 EPYC perf): ~58%/~57% of Write/ ReadTestData self-cycles sat in the generated 17x per-byte write_bits(...,8)/read_bits(8) loop over fixed_bytes. The runtime already has the right primitive - SerializeBytes aligns then memcpys - but the emitter never used it for fixed byte arrays. The wire decision: serialize_bytes' internal align is ZERO bits when the stream is already byte-aligned, and SPEC 4.3 pins bit i of the stream to byte i/8 - so at a byte boundary, N consecutive unranged 8-bit writes are byte-identical to the bulk copy. The emitter may therefore switch ONLY where alignment is statically provable; everywhere else the per-byte loop stays, wire-identical by construction. No golden wire byte moves. ir.AlignedFixedByteArrays is the proof: a conservative bit-position-mod-8 walk over a struct's items (entry unknown - a type embeds at any offset; align and string/bytes wires force known boundaries; branches must agree; counted arrays tracked but not yet marked). The cpp backend consults it in emitWriteField/emitReadField for fixed [N]uint8 arrays and emits write_bytes/read_bytes instead of the loop. Corpus effect: exactly TestData.fixed_bytes (declared after an explicit align). Source goldens re-pinned for that; wire goldens untouched - verified by the full suite: schema_test (wire goldens + wire oracle), variant, random round-trip (2000 iterations), and the go/rust/cs conformance gates all green with SERIALIZE=../serialize. Glenn's approach, verbatim: 'Trick I have used in the past is to align so I can just memcpy in when writing bytes and strings.' Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
rowan-claude
force-pushed
the
optimize-bulk-bytes
branch
from
August 7, 2026 00:25
c3273a8 to
ce1e888
Compare
This was referenced Sep 2, 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.
The C++ emitter now takes serialize's bulk-bytes path (align + memcpy) for fixed
[N]uint8arrays wherever byte alignment is statically provable, instead of emitting a per-bytewrite_bits(...,8)/read_bits(8)loop. Glenn's approach, verbatim: "Trick I have used in the past is to align so I can just memcpy in when writing bytes and strings."The conviction
Serialize PR #27's EPYC profiling put ~58% of
WriteTestDataand ~57% ofReadTestDataself-cycles in the generated 17x per-byte loop overTestData.fixed_bytes. The runtime already had the right primitive —SerializeBytesaligns then memcpys — but the emitter never used it for fixed byte arrays. (Strings andbytes(N)already rodewrite_bytes/read_bytes; the fixed array was the one construct still going byte-at-a-time.)The alignment finding, and the wire decision
SerializeBytesaligns to a byte boundary before the bulk copy, so emitting it blindly would CHANGE THE WIRE at any unaligned site. The decision here is wire-identity restricted to where it is provable:iof the stream to bytei/8, LSB-first — so at a byte boundary, N consecutive unranged 8-bit writes produce exactly the N array bytes in order.SerializeBytes' internal align is ZERO bits when the stream is already aligned (write: zero pad bits emitted; read: zero pad bits consumed, nothing to reject).The proof is
ir.AlignedFixedByteArrays: a conservative bit-position-mod-8 walk over a struct's items. Entry is unknown (a type may embed at any bit offset);alignand the string/bytes(N)wires (§4.7: length, align, payload of whole bytes) force known boundaries; fixed-width fields advance the position; branches must agree on exit; nested structs are walked for their exit; counted arrays are position-tracked but not yet marked. Unknown never optimizes — a wrong "unknown" costs speed, never wire bytes.In the corpus this marks exactly one field:
TestData.fixed_bytes [17]uint8, declared immediately after an explicitalign. Generated delta (both message modes):and the matching
read_byteson the read side.Verification
make SERIALIZE=../serialize test— all green:schema_testOK — includes the wire goldens byte-for-byte (testdata/wire/*.binuntouched by this PR;git diff testdata/wireis empty) and the wire oracle (generated bytes == hand-written classic serialize bytes)schema_test_variantOK,schema_test_randomOK (2000 round-trip iterations)go test ./...OK — source goldens re-pinned for the twoWire.hfiles (source pins may move; wire pins may not, and did not)internal/ir/align_test.go— an 18-type expectation table for the analysis: marked after align / after string / after bytes / whole-byte gaps / agreeing branches / aligned nested exits / byte-wide count prefixes; NOT marked at unknown entry, off-boundary, ranged elements, counted arrays, disagreeing branches, off-boundary or entry-dependent nested exits. Verified the test fails when an expectation is flipped.The bench runner's own gate also passed: it refuses to produce numbers unless the binary's output matches the pinned wire goldens — the new binary benches.
Measurement (M2, tonight — paired, median-of-7, interleaved runs x3)
Apple M2, Release (
-O3 -DNDEBUG -DSERIALIZE_RELEASE, apple clang 21), serialize @6ad407d, bench-harness v2 merged locally for measurement only (not in this diff). Predictions banked before running: write ~2.0x, read ~2.1x, everything else flat.message_batchmoved -7%/+11% in one pair but swings +/-20% between runs of the SAME binary (spreads up to 16.6%) and its generated code is byte-identical in both builds (Messages.hunchanged; the batch carries noTestData) — code-layout noise, not a regression.Refutation, plain: the write prediction was wrong. Read delivered ~1.7-1.8x; write only +5.6%. Root cause, from the runtime source: at serialize
6ad407d,WriteBytesat a byte-but-not-word boundary still pushes up to 15 of the 17 bytes through per-byte head/tailWriteBits— the emitter's loop moved into the runtime's loop. Re-paired against serialize PR #27'soptimize-bytes(packed head/tail,14ea61a): write +14.4% (17.81M -> 20.37M), read +71.5%. So this PR plus serialize PR #27 is the write story so far, and the remaining gap says the EPYC's ~58%-of-write-cycles share does not transfer to the M2 write path as-is. The EPYC pairing is deliberately not run tonight (another session owns that box); worth a quiet-hours profile there before concluding more.Judgment calls
[N]uint8only.int8andbits(8)would also be byte-exact on the wire, but their storage/cast paths differ and nothing profiles them; counted[<= N]uint8is position-tracked but not marked (the write side would need the count prefix emitted before the bulk call — mechanical, unconvicted). All noted in the analysis comments for when a profile convicts them.internal/ir(target-independent wire math, the same charter asMaxBits) so the go/rust/cs backends — whose emitters still write these per-byte loops — can adopt the same proof later.CI note
GitHub Actions is in a major outage today; verification above is local (M2), commands as quoted. The suite should re-run in CI when Actions recovers.