Repository navigation
Write nested length prefixes from the cached size - #45
Merged
Merged
Conversation
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.
Problem
Serializing Pulsar's
BaseCommandis about 13% slower on master than in 0.8.2: 22.8 → 25.9 ns (PulsarApiBenchmark.lightProtoSerializeBaseCommand, JDK 26, median of 29 forks each, run interleaved). Every build has fast and slow forks on this benchmark, and master's usual one is about 3 ns slower than 0.8.2's. Bisecting points to #43. A profile puts the difference in the self time ofBaseCommand._writeTo(), whose bytecode #43 didn't change: 2.7 ns in a fork of the build before #40, 4.4 ns on master.Within
_writeTo(), #43 only changed the nested message'sgetSerializedSize(), which_writeTo()calls for each length prefix and C2 inlines there. A write calls it twice per nested message: the size walk finds the cache empty and computes the size, then_writeTo()finds the size cached. With both outcomes in its profile, C2 compiles the nested message's size walk into the parent's_writeTo(), where it never runs. That was already true before #43, which changed the cache check and the stored value. Without hsdis on this machine I couldn't look at the generated assembly to say exactly why the result got slower.Example
Change
Generated messages get an internal
_sizeForWrite()._writeTo()now uses it for every nested length prefix: singular, oneof, repeated and map message values, in both thebyte[]and the NIO writer._writeTo().writeTo(),toByteArray()and the gRPC marshaller compute the size of the whole tree before they call_writeTo(), so the branch is always taken there. C2 turns the fallback into an uncommon trap instead of compiling the size walk in.getSerializedSize()would. OnlygetSerializedSize()stores a size with the sign bit set, and every change resets the cache to -1. The fallback covers a_writeTo()call made without computing the size first.getSerializedSize(), andparseFrom()is bytecode-identical in the benchmarked messages.Benchmark
JDK 26.0.1, Apple M1 Max, master (9f7549c) and this change in interleaved forks, median ns/op:
_sizeForWrite()against master'sgetSerializedSize(). The row ends about where 0.8.2 was (40.5 ns in those runs).Testing
New
SizeForWriteTestcalls_writeTo()on messages whose sizes were never computed, so every nested prefix takes the fallback. It covers singular and repeated message fields, a message two levels down, map message values (also inside a nested message), and a oneof message. Each is written to abyte[]and to a directByteBufferand compared with protobuf-java. All 6 cases pass before and after this change. Returning the cached size without the fallback fails all 6.mvn verifypasses: 544 tests in each surefire run.