Repository navigation
perf: byte array parser with in-order fast paths, part 2 - #1002
Open
jasperpotts wants to merge 15 commits into
Open
jasperpotts wants to merge 15 commits into
jasperpotts wants to merge 15 commits into
Conversation
Extends the fast_parse array parser (docs/fast-codegen-plan.md section 6.3) to every field kind: - repeated messages, strings and bytes loop on their tag in the fast path; repeated numbers and enums read the packed form there, and the general loop accepts packed and unpacked forms; - oneofs are handled in the switch only, with parseImpl's strict-mode duplicate check; - map entries are read as in parseImpl, without an entry object; - google.protobuf wrapper types are read as in parseImpl. ArrayCursor now returns views for bytes fields when it reads a Bytes, as parseImpl does over Bytes, instead of copies. With every kind supported, a message has an array parser exactly when it has fast_parse. Fixes #1000 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Jasper Potts <1466205+jasperpotts@users.noreply.github.com>
HotSpot does not JIT-compile methods over 8000 bytes of bytecode. With every field kind in parseArray, large messages crossed it: Everything's parseArray was 8435 bytes and ran interpreted, 45% slower than parseImpl on AMD EPYC, and TransactionBody's was 9198 bytes. Nested message, wrapper type, repeated element and packed-run reads now go in small per-field static methods, called from both the fast path and the general loop. Everything's parseArray is 4877 bytes and TransactionBody's 5136; the JIT still inlines the helpers where they are hot. Refs #1000 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Jasper Potts <1466205+jasperpotts@users.noreply.github.com>
Member
Author
|
Merged in the review follow-ups from below in the stack: exact-size bulk varint writes (#994), unique writer local names (#995), dead-code removal (#996), map entries without try/finally plus tests (#997), and strict string decoding without per-call allocations (#1010). On #999 the array parser's |
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/CodecParseArrayMethodGenerator.java
…, oneofs and maps MixedFastCollections and MixedSlowCollections hold repeated fields, a oneof and maps of messages with and without fast_parse, so the generated tests round-trip them through both parsers. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Jasper Potts <1466205+jasperpotts@users.noreply.github.com>
This was referenced Oct 30, 2026
Member
Author
|
Review follow-ups (second review, section 5.1 and section 2):
|
Signed-off-by: Jasper Potts <1466205+jasperpotts@users.noreply.github.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>
Signed-off-by: Jasper Potts <1466205+jasperpotts@users.noreply.github.com>
Signed-off-by: Jasper Potts <1466205+jasperpotts@users.noreply.github.com>
Signed-off-by: Jasper Potts <1466205+jasperpotts@users.noreply.github.com>
Signed-off-by: Jasper Potts <1466205+jasperpotts@users.noreply.github.com>
Signed-off-by: Jasper Potts <1466205+jasperpotts@users.noreply.github.com>
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 is part 2 of the opt-in byte-array parser (
docs/fast-codegen-plan.mdsection 6.3,pbj.fast_parse). Every field kind is now supported, so a message has an array parser exactly when it hasfast_parse.Performance (AMD EPYC 9275F): with
fast_parse, a whole HAPI block parses 2.3× faster (46.5 → 20.4 ms, about 3× faster than protoc), block items 1.9× (548 → 290 ns), signed transactions 1.4×, and allocation is down 4-27%. Tier B, opt-in.while (input.at(tag))), withparseImpl's list-sizemaxSizecheck.parseImpl, unknown fields inside an entry go to the enclosing message's unknown fields.google.protobufwrapper types are read asparseImplreads them.bytesfields fromparse(Bytes)are now views of the input, asparseImplreturns them, rather than copies. This fixes the parity gap noted in perf: byte array parser with in-order fast paths, part 1 #999.Everything'sparseArraygrew to 8,435 bytes and ran interpreted, 45% slower thanparseImpl;TransactionBody's grew to 9,198. So nested message, wrapper, repeated-element and packed-run reads are now small per-field static methods, which the fast path and the general loop both call. That brings the two methods down to 4,877 and 5,136 bytes, and the JIT still inlines the helpers where they're hot.Not in this PR: list presizing (plan 5.3). It isn't needed for these gains, and I'm leaving it until a profile shows list growth matters.
Pre-existing finding, for a follow-up (tier A). The stream parsers of several large HAPI messages are already over the 8,000-byte limit on
main, so HotSpot runs them interpreted:parseFrom)TransactionBodyStateValueSchedulableTransactionBodyThe same per-field helper technique would fix them for every Hiero parse.
Generated code:
StateChanges(excerpt)Tests
pbj-core:spotlessApply qualityGate buildpasses.ArrayCursorTestcovers views versus copies, andPerformanceOptionsTestcovers repeated fields getting array parsers.pbj-integration-tests,./gradlew build -PpbjPerformance=max: 326,405 tests pass, with an array parser on all 578 HAPI and test messages. That includes the protoc comparisons, the non-canonical-order tests (the fallback loop),MaxDepthTest,MaxSizeTest,UnknownFieldsTest,MalformedMessageTest,TruncatedDataTests,ParseLimitRestoreTest, the fuzz tests, andAllocationTest, whose parse ceilings hold. Allocation is lower for every kind of block item; for exampleBLOCK_HEADERdrops from 1,048 to 416 B../gradlew build(options off) passes, 326,405 tests.Performance: AMD EPYC 9275F, before = #999 with
fast_parseon, after = this PR withfast_parseon3 forks, 3 × 1 s warmup, 5 × 1 s measurement,
-prof gc. Parses are measured fromBytes(BlockItemBench) and from heapBufferedData(ProtobufObjectBench,SampleBlockBench).AccountDetails1.9×.Everything(every field type) parses 2.4× faster (27.0 → 11.3 µs), now faster than protoc's 14.2 µs.bytesviews and noArrayCursorper nested message.parseImpl. I have not established why; possibly better JIT budget once the nested array-capable codecs are compiled. I'm reporting it as measured.This is in line with the plan's 2.5-3× for nested messages.
Generated code size (full HAPI build)
main-PpbjPerformance=max, this PR*ProtoCodecclassesWith options off, generated main code is unchanged. Tier B: the parse code of flagged messages roughly doubles, as the plan predicted.
Stacked on #999.
Fixes #1000
🤖 Generated with Claude Code