Repository navigation
Compute the payload size of packed varint fields once per serialization - #41
Merged
Merged
Conversation
getSerializedSize() walks the elements of a packed varint field to size its payload, and _writeTo() walked them again for the payload's length prefix. getSerializedSize() now keeps the payload size in a field, and _writeTo() uses it. Every serialization runs getSerializedSize() right before _writeTo(), and every change to the elements resets _cachedSize, so getSerializedSize() recomputes the payload size whenever the elements changed. parseFrom() caches the message size without computing any field, so parsing the field resets its payload size, and _writeTo() computes and keeps it when it is unknown. clear() is unchanged: resetting there cost every parse one store per packed field, present or not. Fixed-width packed fields already size their payload as count times width, so only messages with packed varint fields change; proto3 repeated scalars are packed by default. JDK 26, one packed int64 field: writeTo() with the size already cached 4761 -> 3840 ns for 1200 values and 34.1 -> 28.3 ns for 10, after a change 5883 -> 4808 ns and 43.8 -> 37.8 ns. Parsing is within 1%.
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 a packed varint field walks its elements twice just to size it.
getSerializedSize()sums the element sizes to size the payload, then_writeTo()sums them again to write the payload's length prefix. Packed is the default for repeated scalars in proto3, so this affects every proto3 message with a repeated integer or enum field, plus proto2 fields marked[packed = true].Example
The generated
_writeTo()forrepeated int64 x_int64 = 2 [packed = true]repeats the loop thatgetSerializedSize()just ran:Change
getSerializedSize()now keeps the payload size in a new field, and_writeTo()uses it:Why the cached value stays correct:
getSerializedSize()just before_writeTo().addX(),clearX(),clear(),copyFrom()) resets_cachedSize, sogetSerializedSize()recomputes the payload size whenever the elements changed.parseFrom(), which caches the message size without computing any field. So parsing the field resets its payload size to -1, and_writeTo()computes it and keeps it when it's unknown.clear()is unchanged. Resetting the payload size there instead cost every parse one store per packed field, whether the field was present or not: +3.5% on a 10-element parse of a message with 7 packed fields.Fixed-width packed fields already size their payload as count × width. So only messages with packed varint fields change; Pulsar's generated code is byte-identical.
JDK 26, one packed
int64field, 3 interleaved rounds (median ns/op):writeTo, size already cachedwriteToafter a changeTesting
The new
PackedSizeTestchanges the elements after the size was cached in six ways: adding,clearX(),clear(), parsing, re-serializing a parsed message and then adding, andcopyFrom(). Each case runs on both write paths (heap array, and the NIO view of a direct buffer) and is compared byte for byte with protobuf-java. Without the parse-time reset, the parse case fails on both paths.mvn verifypasses (554 tests).