Skip to content

v0.9.1

Choose a tag to compare

@merlimat merlimat released this 10 Oct 16:58
· 5 commits to master since this release

gRPC: faster than grpc-java's protobuf marshaller

The generated gRPC marshaller no longer copies large bytes values twice on the way out, and parsing a received message allocates less. On the new GrpcSerdeBenchmark (#47), LightProto now serializes and deserializes faster than grpc-java's protobuf marshaller, and allocates less, on every shape measured. The one exception is deserializing a single 64 KiB value, where the two are even.

Write large bytes values from their own buffers in the gRPC marshaller (#49)

stream() serialized the whole message into an array, which gRPC's MessageFramer then copied into its transport buffers. A large bytes value was copied twice, and a 64 KiB value serialized 2.1x slower than with protobuf. A bytes value of 16 KiB or more, held in a heap ByteBuf or a byte[], is now written to the transport straight from its own memory. A 64 KiB value now serializes in 960 ns instead of 2.2 µs. Messages under 16 KiB take the same path as before.

Behavior change: the stream keeps a reference to such a value until gRPC closes the stream, right after framing the message. A caller may release a ByteBuf once the send call returns, because the stream retains it, but must not change a large value's contents before then.

Parse single-buffer gRPC messages through a reused view (#50)

A message received in one direct transport buffer, which over Netty is every message under 16 KiB, was wrapped in a new Netty ByteBuf for parsing. Each thread now parses through one reused view instead. Deserializing a 100-byte message: 72 ns and 784 B before, 59.5 ns and 656 B after.

Store bytes values as byte[] instead of wrapping them in a ByteBuf (#51)

setX(byte[]), addX(byte[]), putX(key, byte[]) and materialize() wrapped every array in a new UnpooledHeapByteBuf, 48 bytes per value. The gRPC marshaller materializes every message it parses, so each value received paid for one. Arrays are now kept as they are. Getters return them without copying, as before. Parsing 4 KiB of records through the marshaller is 14% faster and allocates 21% less; serializing them is 8-10% faster. getXSlice() of an array value now returns a new buffer over the array, not a slice of a shared wrapper.

Tests and build

Upgrade grpc-java to 1.84.1 (#48)

LightProto's tests and benchmarks now run against grpc-java 1.84.1. Since grpc-java 1.82.4, gRPC merges runs of small transport buffers (under 1 KiB) into one array that offers no ByteBuffer access. A message that starts with merged buffers is now copied by the marshaller instead of parsed in place. The tests cover both cases.

Add gRPC serde benchmark comparing protobuf and LightProto marshallers (#47)

GrpcSerdeBenchmark serializes and parses the same messages through grpc-java's protobuf marshaller and LightProto's. It goes through gRPC's own MessageFramer and transport buffer types.

Upgrade actions/checkout and actions/setup-java to v5 (#46)

Publish with Maven 3.9.16 (414f354)

GitHub's runner image now ships Maven 3.10.0, which adds its local-repository files to the bundle that central-publishing-maven-plugin 0.11.0 uploads, and Maven Central rejects that bundle. The publish workflow now installs Maven 3.9.16, pinned by its SHA-512.

Performance vs v0.9.0

JDK 26, Apple M5 Pro, six interleaved A/B rounds (one fork per version per round), JMH 3 warmup and 5 measured iterations of 1 s. Time per operation, lower is better; Δ is the median of the per-round ratios. Allocation is shown where it changed.

v0.9.0 v0.9.1 Δ
gRPC deserialize (GrpcSerdeBenchmark)
one 100 B value 73.1 ns, 784 B 55.7 ns, 616 B -23.9%
one 4 KiB value 195.1 ns, 4776 B 168.4 ns, 4608 B -13.8%
one 64 KiB value 2435 ns 2438 ns -0.6%
1 record (100 B) 91.0 ns, 880 B 79.4 ns, 712 B -12.8%
64 records (4 KiB) 1983 ns, 15.0 KB 1651 ns, 11.8 KB -17.4%
1024 records (64 KiB) 33.3 µs, 231 KB 29.2 µs, 182 KB -11.7%
gRPC serialize (GrpcSerdeBenchmark)
one 100 B value 100.0 ns 99.2 ns -1.9%
one 4 KiB value 204.8 ns 197.3 ns -1.7%
one 64 KiB value 2231 ns, 55 B 960 ns, 199 B -57.5%
1 record (100 B) 104.6 ns 105.7 ns -1.0%
64 records (4 KiB) 752 ns 676 ns -10.0%
1024 records (64 KiB) 12.0 µs 10.8 µs -9.9%
README benchmarks
AddressBook serialize 23.6 ns 23.8 ns +1.3%
AddressBook fill + serialize 37.1 ns 37.7 ns +1.6%
AddressBook deserialize 22.1 ns 22.8 ns +3.4%
Pulsar BaseCommand serialize 13.2 ns 12.8 ns -2.1%
Pulsar MessageMetadata serialize 41.3 ns 41.6 ns +0.8%
Pulsar BaseCommand deserialize 10.5 ns 10.5 ns +1.0%
Pulsar MessageMetadata deserialize 23.8 ns 23.8 ns -0.3%
Simple serialize 3.94 ns 4.01 ns +1.9%
Simple deserialize 6.38 ns 6.52 ns +2.0%
Simple deserialize + read string 11.05 ns 11.17 ns +0.2%
  • Of the README benchmarks' messages, only Pulsar's MessageMetadata has bytes fields.
  • The other messages' parse code is byte-identical to v0.9.0's (javap). Their writers changed only in a branch these benchmarks don't take: re-serializing a string that was never decoded. So the ±3% on those rows, the AddressBook deserialize row included, is run-to-run noise.
  • Against grpc-java's protobuf marshaller on the same machine (measured separately), protobuf deserializes these messages in 57 ns / 656 B (one 100 B value), 184 ns / 4648 B (one 4 KiB value), 2408 ns / 66408 B (one 64 KiB value), 97 ns / 760 B (1 record), 2491 ns / 13.2 KB (64 records) and 37.7 µs / 194 KB (1024 records). It serializes them in 140 ns, 339 ns, 1160 ns, 183 ns, 1425 ns and 20.7 µs, allocating 0.5 KB to 37 KB per message.