Repository navigation
perf: 1029: writers pay less per nested message, and nothing for small ones - #1033
Open
jasperpotts wants to merge 14 commits into
Open
jasperpotts wants to merge 14 commits into
jasperpotts wants to merge 14 commits into
Conversation
A CHILD guard value marks an open nested message, so ending it on the parent's next field is a small non-recursive-in-practice call the JIT inlines, instead of checkSlow (89 bytes, not inlined) plus closeInside. varintWide writes 2- and 3-byte varints inline. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Jasper Potts <1466205+jasperpotts@users.noreply.github.com>
Open, ended and written-by-writeFrom are guard values, so opening and closing a nested message store one field less each. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Jasper Potts <1466205+jasperpotts@users.noreply.github.com>
The offset is the start, updated when the bytes move, and a one-byte length that fits leaves the adaptive guess as it is. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Jasper Potts <1466205+jasperpotts@users.noreply.github.com>
Signed-off-by: Jasper Potts <1466205+jasperpotts@users.noreply.github.com>
…guess A message field whose type has at most four integer or bool fields (ids, timestamps; a oneof may have one integer alternative) gets an overload on the parent writer that writes it from its fields' values in one call: tag, length computed from the values, fields. No child writer is opened. Children whose largest encoding is under 128 bytes open with openSmallChild, a one-byte length without consulting the reservation. The synthetic workload's writer code uses the new overloads. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Jasper Potts <1466205+jasperpotts@users.noreply.github.com>
Enums are passed as their ordinals. On HAPI this adds 4 overloads to the 413 of the four-field limit, with no measurable change in class size. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Jasper Potts <1466205+jasperpotts@users.noreply.github.com>
A repeated field whose element type qualifies gets addXxx(values...) too, writing one element per call. On HAPI: 22 more overloads, 439 in all. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Jasper Potts <1466205+jasperpotts@users.noreply.github.com>
Values from 2^56 to 2^63, such as int64 nanosecond times, went through a call to ProtoArrayWriterTools that the JIT did not inline, which an EPYC profile of an external block workload put at 8.5% of encoding. They are now written inline: the low 56 bits as eight continued groups of seven in one little-endian store, then the last byte. Other sizes are unchanged, as one 8-byte store for three to eight bytes measured slower on EPYC. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Jasper Potts <1466205+jasperpotts@users.noreply.github.com>
ProtoArrayWriterTools.writeVarIntWide, which writers called for varints of four bytes and more, is 437 bytes of bytecode, over the 325 the JIT inlines for hot methods, so every such value was a call. The four- to eight-byte cases are now in ProtoWriteBuffer.varint4To8, the same exact-size stores in 316 bytes, which the JIT inlines; negative values, ten bytes, still call it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Jasper Potts <1466205+jasperpotts@users.noreply.github.com>
This reverts commit f2c68bb. On AMD EPYC, with an external block workload, it made block encoding 5-9% slower and transaction encoding 9% slower, reproduced in two runs: inlining a 316-byte writer at every varint site of the large generated write methods cost more than the call it saved. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Jasper Potts <1466205+jasperpotts@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Jasper Potts <1466205+jasperpotts@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Jasper Potts <1466205+jasperpotts@users.noreply.github.com>
This reverts commit e134322. On AMD EPYC it was neutral: writers +1.6% and -1.5% hashed on the HAPI workload, within noise, and neutral on an external block workload full of nine-byte nanosecond times. The plan's lessons record it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Jasper Potts <1466205+jasperpotts@users.noreply.github.com>
JUnit Test Report 559 files 559 suites 1m 18s ⏱️ Results for commit de84353. ♻️ This comment has been updated with latest results. |
Integration Test Report 660 files 660 suites 44m 41s ⏱️ Results for commit de84353. ♻️ This comment has been updated with latest results. |
Closed
Signed-off-by: Jasper Potts <1466205+jasperpotts@users.noreply.github.com>
jasperpotts
force-pushed
the
1029-writer-open-close
branch
from
November 2, 2026 20:33
e0fb5c6 to
de84353
Compare
jasperpotts
force-pushed
the
1027-synthetic-block-bench
branch
from
November 2, 2026 20:33
a0a4673 to
2fe51fe
Compare
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.
Closes #1029. Stacked on #1028 (
1027-synthetic-block-bench).Summary
The flyweight writers (#1023) paid most of their time per nested message: opening a child, checking every field, and patching its length when it ended. This PR cuts that bookkeeping, and for small messages removes it entirely. Everything here is in the writers, so it only applies with
pbj.writerson (tier B); models, codecs and the default configuration are unchanged.Performance (AMD EPYC 9275F): on the synthetic HAPI block, writing through the writers is 18.9% faster (1,381 → 1,121 ns per transaction) and still allocates 0 B; block writes with SHA-384 per item are 6.5% faster. Tier B, opt-in with
pbj.writers.guardfield (531cbf59,698ba8e6,a96d0161):Integer.MAX_VALUE), "a child is open" (MAX_VALUE - 1) and "written bywriteFrom" (MAX_VALUE - 2), so checking a field stays one compare, and ending an open child moves onto that compare's slow path.stateandoffsetfields are gone, and closing a nested message stores less.endChild()is small enough for the JIT to inline into the field checks.5493cdac,56f153f3,987f5f07): a message with up to six number, bool or enum fields, and no repeated fields, maps or other messages, gets a setter on its parent's writer that takes the fields as arguments. It computes the length and writes the tag, length and fields directly, with no child writer and nothing to close:openSmallChild): their length is always one byte, whatever the buffer's length reservation.Tried and reverted (recorded in
docs/fast-codegen-plan.md§9)EPYC results
AMD EPYC 9275F, JDK 25.0.4, one CCD (
taskset -c 0-2,24-26) of an idle machine, 3 forks × 5 iterations of 2 s,-prof gc. ns per transaction of the synthetic HAPI block (#1028), every option on.produceWritersproduceWritersBlockHashedThe one-call setters are the whole win; the state changes before them are neutral, and kept because the writer is simpler with them. In the full benchmark run at each PR head,
produceWriterswent from 1,359 to 1,146 ns (−15.7%), which is 1.52× the hand-written reference encoder (756 ns in the same run), down from 1.79×.produceWritersBlockHashedincludes SHA-384 of every item.API
The one-call setters add public methods to generated writers (439 in HAPI). They are overloads next to the existing child accessors, so nothing is removed or renamed, but it is new surface to review.
Tests
InlineSetterTest: ids (with their oneof's one number alternative), timestamps and repeated elements written in one call give the same bytes as the child writer and as the model, for zero, small, large and negative values, and the field order is still checked.WriterGenerationTest: which messages get one-call setters, enums included: six fields qualify, while seven, a string field or two number alternatives of one oneof don't; and small children are opened withopenSmallChild.🤖 Generated with Claude Code