Repository navigation
perf: generate straight-line size calculation and stream writers - #996
Open
jasperpotts wants to merge 9 commits into
Open
jasperpotts wants to merge 9 commits into
jasperpotts wants to merge 9 commits into
Conversation
Applies the straight-line code of the byte-array writer to the model's protobufSize() and to the stream writer (docs/fast-codegen-plan.md section 6.1, items 1-3 and 6): - protobufSize() uses literal tag sizes, inline default checks and the nested model's own protobufSize() instead of Codec.measureRecord; - the stream writer writes constant tags with writeByte/writeByte2..4, checks defaults inline, and calls a new public static writeTo(T, WritableSequentialData) on nested codecs from the same build; - Codec.toBytes writes through write(T, byte[], 0), the array writer. One generator, InlineFieldCodeGenerator, now produces the inline code for all three targets; the byte-array writer moves into it unchanged. Output bytes and sizes are unchanged. Fixes #990 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Jasper Potts <1466205+jasperpotts@users.noreply.github.com>
generateMeasureMethod built field size lines for a placeholder the template no longer has; measureRecord returns data.protobufSize(). The helpers stay, for map entries. Refs #990 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Jasper Potts <1466205+jasperpotts@users.noreply.github.com>
…-writer-codegen Signed-off-by: Jasper Potts <1466205+jasperpotts@users.noreply.github.com> # Conflicts: # pbj-core/pbj-compiler/src/main/java/com/hedera/pbj/compiler/impl/generators/protobuf/CodecWriteByteArrayMethodGenerator.java
Member
Author
|
Review follow-ups
|
This was referenced Oct 30, 2026
…-writer-codegen Signed-off-by: Jasper Potts <1466205+jasperpotts@users.noreply.github.com>
…-writer-codegen Signed-off-by: Jasper Potts <1466205+jasperpotts@users.noreply.github.com>
This was referenced Oct 30, 2026
…-writer-codegen Signed-off-by: Jasper Potts <1466205+jasperpotts@users.noreply.github.com>
Closed
| .formatted(oneOfField.nameCamelFirstLower(), oneOfType, Common.camelToUpperSnake(field.name())); | ||
| prefix = oneOfGuard | ||
| ? prefix | ||
| + "if (data.%s().kind() == %s.%s)%n" |
Contributor
There was a problem hiding this comment.
Many review comments from #995 apply to this fix, including the newly introduced class here, because they iterate on the same code.
Comment on lines
+126
to
+128
| prefix += "\n"; | ||
| // The statement below is the (unbraced) body of the "if" above, so indent it one level | ||
| // deeper than the "if" itself to make that visually clear. |
Contributor
There was a problem hiding this comment.
Please don't generate unbraced multi-line if's. Removing the \n might be the nicest fix.
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.
What and why
This PR applies the straight-line approach from #995 to the other two hot paths (
docs/fast-codegen-plan.mdsection 6.1, items 1-3 and 6):Performance (AMD EPYC 9275F):
toBytesof a freshly built block item is 2.1× faster (968 → 452 ns), of a signed transaction 1.7× (96 → 58 ns), and whole-block stream writes are 15-23% faster. Step 1 as a whole (#994-#996) makes block item array writes 28-30% faster. Tier A, on by default.protobufSize()in the model. It now uses literal tag sizes and inline default checks, and measures each nested message through that model's ownprotobufSize(). That is a direct call on the model type. Before, it went throughCodec.measureRecordon the shared, megamorphic call site. Every block item that is built and written once is measured once, so this is part of every fresh write.write(T, WritableSequentialData). It writes constant tags withwriteByte/writeByte2..4and checks defaults inline. Each codec gets a newpublic static void writeTo(T, WritableSequentialData), which nested messages from the same build call directly.Codec.toBytesnow writes throughwrite(T, byte[], 0), the array writer, instead of the stream writer wrapped around aBufferedData.InlineFieldCodeGeneratorproduces the inline code for all three targets: byte array, stream and size. The field order, default checks, oneof switches and fallbacks are written once. The byte-array writer from perf: generate straight-line byte array writers with direct nested calls #995 moves into it unchanged.ProtoWriterTools.sizeOfUnsignedVarInt64andsizeOfStringNoTagbecome public, for generated models. NewwriteStringNoTagandwriteBytesNoTagstream helpers are added.Generated code:
TransactionID.protobufSize()before and afterBefore:
After:
The stream writer, for the same message:
Tests
ByteArrayWriterGenerationTestis extended: the stream writer andprotobufSize()use direct calls for messages from this build, and theCodec/sizeOfMessagehelpers for dependencies.pbj-core:spotlessApply qualityGate buildpasses.pbj-integration-tests:./gradlew buildand-PpbjPerformance=maxboth pass, 326,323 tests each. The protoc comparisons checkmeasureRecord, stream writes andtoBytesfor every test object.Performance: AMD EPYC 9275F, before = #995's branch, after = this PR
3 forks, 3 × 1 s warmup, 5 × 1 s measurement,
-prof gc.toBytesof a freshly built block item is 2.1× faster (968 → 452 ns), and 1.7× for a signed transaction (96 → 58 ns), with 80 B/op less allocated. This is the measuring pass plus routingtoBytesto the array writer.SampleBlockBenchinto heap and directBufferedData, andtoBytes. Writing to anOutputStreamis unchanged (-4%, within noise), because the stream itself dominates.write(T, byte[])for fresh block items is 13% faster, because measuring is part of it. Cached-size writes and signed transactions are unchanged within noise.Step 1 as a whole (#994 → this PR),
BlockItemBenchper block item:write(byte[]), freshly builtwrite(byte[]), sizes cachedtoBytes, freshly builttoBytesThe plan expected about 2× for writing deeply nested messages. Step 1 reaches it for
toBytesof fresh block items, and reaches 1.4× for the array write itself.Generated code size (full HAPI build)
mainmain)mainclasses*ProtoCodecclassesThe plan expected step 1 to shrink generated code; it grows slightly, well within the 15% tier A limit.
Stacked on #995.
Fixes #990
🤖 Generated with Claude Code