Generated sizes for small writes in the native serializer#

This report measures the generated-size path added for #112 . After the sizing-traversal change , scalar and nested serialize and serializeSameInstance still took 2.4 to 2.9 times Chill’s time, collections writes 2.0 to 2.5 times, and scalar and nested buildAndSerialize 1.5 to 1.6 times, while a diagnostic variant without safe sizing met the ADR-0004 tolerance in almost every cell. One plan on temporary GCE VMs compares the change with the unchanged serializer. The measurement contract is ADR-0004 ; the comparison does not establish whole-job throughput or state compatibility.

Change under test#

Before the change, every write sized the record with the iterative traversal, which reads each field through Protobuf’s reflective accessors. The changed serializer first decides whether the payload type can use the runtime’s memoized getSerializedSize(). That requires an acyclic reachable type graph whose static height, the greatest parser depth a value can reach without unknown groups, is within the recursion limit. For such a type, the serializer reads only the value’s message fields to find unknown groups and values reporting another descriptor, and its repeated strings and string-valued maps to bound their wire size from below. It then uses the generated size if that size is at most 4 KiB and the size limit, and confirms it by encoding into the existing 4 KiB scratch array; a size that wrapped around int implies at least 2 GiB of output, so that encoding fails and the traversal reports the exceeded limit. Every other value, including every payload above 4 KiB, uses the traversal as before. ADR-0001 records the conditions and the amended write-side rule; ProtobufGeneratedSizeTest compares the path’s decisions with the traversal’s.

The benchmark record is acyclic with static height 1: one nested message field without message fields of its own, and a map with int64 values. Its small workloads therefore use the generated size with one reflective read per write, and the large workload, whose payload exceeds 4 KiB, uses the traversal after that read.

Method#

The plan used the temporary GCE procedure in project flink-gcp. Each Protobuf profile ran on its own c3-standard-8 VM in asia-northeast1-b (Intel Xeon Platinum 8481C, Sapphire Rapids, 8 vCPUs, 32 GiB) from image ubuntu-2404-noble-amd64-v20261021, kernel 7.0.0-1014-gcp, with Temurin 17.0.20.1, Flink 2.3.0, and Protobuf/protoc 3.25.9 or 4.33.6. Both VMs recorded TERMINATE host maintenance and NONE as the only maintenance event, passed the load gate before every build and measurement, and reported the same machine type, CPU platform, image, kernel, and JDK. just benchmark-pair ran the ADR-0004 long-run protocol for the native and Chill Protobuf lanes, and each variant took about 70 minutes.

PlanSource and baseVariants, in run orderTime (UTC)
issue112-sizing-2ee63f1c8da68cdcad4c961afb71b7754acfbbd3d on 0c40c0bbaseline, candidate2026-10-22, 03:21 to 05:53

The baseline is the previous serializer and its tests, identical to the base under src/main; the candidate is the source commit unchanged.

Comparisons between the variants use the Chill lane as a per-block control, as in the earlier reports: the ratio of the two variants’ geometric-mean native/Chill time ratios, with a pooled two-sample Student-t interval on the five log ratios of each run (8 degrees of freedom). Below 1 favors the candidate. Allocation is the five-fork mean of JMH’s normalized allocation, in bytes per operation. These intervals are per cell, without a simultaneous claim across cells.

Results#

Writes#

Each variant cell is native time in ns/op and allocation in B/op, as five-fork means. The last three columns give the candidate’s time relative to the baseline and each variant’s native/Chill interval and ADR-0004 verdict.

Protobuf 4:

WorkloadOperationBaselineCandidateCandidate/baselineBaseline native/ChillCandidate native/Chill
scalarserialize271 / 34658 / 1360.217 (0.204, 0.230)2.774 (2.591, 2.968) missed0.601 (0.586, 0.616) established
scalarserializeSameInstance231 / 28046 / 960.200 (0.190, 0.210)2.693 (2.555, 2.839) missed0.538 (0.524, 0.552) established
scalarbuildAndSerialize487 / 1010226 / 7330.463 (0.449, 0.477)1.576 (1.536, 1.617) missed0.730 (0.712, 0.748) established
textserialize1254 / 346549 / 1360.414 (0.381, 0.449)1.591 (1.524, 1.662) missed0.658 (0.602, 0.720) established
textserializeSameInstance1058 / 312611 / 1360.562 (0.477, 0.663)1.409 (1.328, 1.495) missed0.792 (0.655, 0.958) established
textbuildAndSerialize1903 / 20341217 / 17680.636 (0.587, 0.689)1.295 (1.209, 1.389) missed0.823 (0.770, 0.881) established
nestedserialize326 / 35781 / 1360.247 (0.241, 0.253)2.670 (2.592, 2.751) missed0.660 (0.657, 0.662) established
nestedserializeSameInstance260 / 31268 / 1360.264 (0.256, 0.272)2.364 (2.283, 2.448) missed0.624 (0.617, 0.631) established
nestedbuildAndSerialize575 / 1021270 / 7440.476 (0.462, 0.491)1.554 (1.508, 1.601) missed0.740 (0.725, 0.755) established
collectionsserialize2201 / 666838 / 1360.363 (0.325, 0.405)2.461 (2.276, 2.660) missed0.893 (0.803, 0.992) established
collectionsserializeSameInstance1960 / 600760 / 1420.388 (0.363, 0.415)2.416 (2.241, 2.605) missed0.937 (0.913, 0.963) established
collectionsbuildAndSerialize3663 / 39801690 / 17360.465 (0.446, 0.485)1.974 (1.925, 2.023) missed0.918 (0.880, 0.959) established
largeserialize3586 / 44903633 / 45061.018 (0.997, 1.040)0.338 (0.331, 0.347) established0.345 (0.341, 0.348) established
largeserializeSameInstance2695 / 44562703 / 44721.011 (0.989, 1.032)0.271 (0.268, 0.275) established0.274 (0.268, 0.280) established
largebuildAndSerialize12016 / 7069512006 / 707060.998 (0.985, 1.010)0.648 (0.640, 0.657) established0.647 (0.642, 0.652) established

Protobuf 3:

WorkloadOperationBaselineCandidateCandidate/baselineBaseline native/ChillCandidate native/Chill
scalarserialize273 / 34661 / 1360.222 (0.212, 0.232)2.777 (2.710, 2.844) missed0.616 (0.588, 0.646) established
scalarserializeSameInstance232 / 28847 / 960.203 (0.192, 0.215)2.657 (2.489, 2.836) missed0.540 (0.528, 0.552) established
scalarbuildAndSerialize483 / 1010229 / 7500.475 (0.463, 0.487)1.573 (1.537, 1.611) missed0.747 (0.732, 0.761) established
textserialize1273 / 346971 / 1360.687 (0.420, 1.124)0.747 (0.689, 0.810) established0.514 (0.286, 0.923) established
textserializeSameInstance1147 / 3121120 / 1361.031 (0.524, 2.028)0.674 (0.605, 0.750) established0.694 (0.310, 1.558) inconclusive
textbuildAndSerialize1952 / 20391541 / 17680.719 (0.482, 1.071)0.598 (0.561, 0.636) established0.429 (0.267, 0.692) established
nestedserialize329 / 35787 / 1360.260 (0.222, 0.304)2.744 (2.608, 2.888) missed0.713 (0.595, 0.855) established
nestedserializeSameInstance266 / 31274 / 1360.278 (0.241, 0.319)2.449 (2.408, 2.491) missed0.680 (0.575, 0.804) established
nestedbuildAndSerialize577 / 1021299 / 7440.519 (0.450, 0.598)1.576 (1.533, 1.619) missed0.817 (0.690, 0.968) established
collectionsserialize2292 / 666863 / 1360.366 (0.332, 0.403)2.473 (2.341, 2.612) missed0.905 (0.816, 1.003) established
collectionsserializeSameInstance1960 / 613776 / 1360.398 (0.390, 0.405)2.352 (2.313, 2.392) missed0.936 (0.922, 0.950) established
collectionsbuildAndSerialize3952 / 41031856 / 21200.481 (0.444, 0.520)1.980 (1.858, 2.110) missed0.952 (0.888, 1.020) established
largeserialize3579 / 44903603 / 45061.020 (0.981, 1.061)0.325 (0.314, 0.337) established0.332 (0.322, 0.343) established
largeserializeSameInstance2738 / 44562747 / 44721.006 (0.986, 1.026)0.270 (0.265, 0.275) established0.271 (0.267, 0.276) established
largebuildAndSerialize12294 / 7067912351 / 706951.003 (0.997, 1.009)0.647 (0.643, 0.651) established0.649 (0.646, 0.652) established

On both profiles, the candidate takes 0.20 to 0.28 times the baseline’s time for scalar and nested serialize and serializeSameInstance, 0.36 to 0.40 for collections serialize and serializeSameInstance, and 0.46 to 0.52 for scalar, nested, and collections buildAndSerialize, and each of those intervals lies below 1. Protobuf 4 text writes take 0.41 to 0.64 times the baseline’s time. Previously encoded scalar, text, nested, and collections records now allocate 96 to 142 B/op, against 280 to 666 B/op before; buildAndSerialize keeps the allocation of building the record.

Every small write except Protobuf 3 text serializeSameInstance, which is inconclusive, now meets the native/Chill tolerance. Scalar and nested serialize and serializeSameInstance take 0.54 to 0.71 times Chill’s time, scalar and nested buildAndSerialize 0.73 to 0.82, collections writes 0.89 to 0.95, and Protobuf 4 text writes 0.66 to 0.82.

Large writes are unchanged within the intervals, all of which contain 1, and remain established at 0.27 to 0.65 times Chill’s time. They allocate 11 to 16 B/op more, which is consistent with the one reflective read before the traversal; this report does not profile it.

Protobuf 3 text writes vary more within each run than the other cells. The standard deviation of the candidate’s five log native/Chill ratios is 0.38 to 0.65 for them, against at most 0.15 for the other write cells, so their candidate-versus-baseline intervals are wide and contain 1, and serializeSameInstance is inconclusive at 0.694 (0.310, 1.558); the earlier reports recorded the same spread for these cells on Protobuf 3.

ProfileVariantEstablishedInconclusiveMissed
Protobuf 4, 40 cellsbaseline26014
Protobuf 4, 40 cellscandidate3802
Protobuf 3, 40 cellsbaseline29011
Protobuf 3, 40 cellscandidate3712

The remaining misses on both profiles are large deserialize and deserializeReuse, at 1.255 to 1.274 times Chill’s time, which #113 addresses. The large-read report measures that later change.

Reading and copying#

Reading, stream copy, and object copy run identical code in both builds.

Protobuf 4:

WorkloadOperationBaselineCandidateCandidate/baselineBaseline native/ChillCandidate native/Chill
scalardeserialize110 / 352112 / 3521.039 (0.965, 1.118)0.640 (0.624, 0.657) established0.665 (0.611, 0.724) established
scalardeserializeReuse117 / 352117 / 3520.991 (0.948, 1.036)0.670 (0.643, 0.698) established0.664 (0.642, 0.687) established
scalarstreamCopy15 / 014 / 00.958 (0.933, 0.984)0.041 (0.041, 0.041) established0.039 (0.038, 0.041) established
scalarobjectCopy1 / 01 / 01.025 (0.957, 1.099)0.006 (0.005, 0.006) established0.006 (0.006, 0.006) established
scalarobjectCopyReuse2 / 02 / 01.010 (0.995, 1.025)0.008 (0.008, 0.008) established0.008 (0.008, 0.009) established
textdeserialize1228 / 41041227 / 41041.000 (0.985, 1.015)0.823 (0.811, 0.835) established0.823 (0.813, 0.832) established
textdeserializeReuse1226 / 41041225 / 41040.995 (0.983, 1.007)0.814 (0.808, 0.821) established0.810 (0.800, 0.820) established
textstreamCopy50 / 050 / 01.013 (0.994, 1.034)0.018 (0.017, 0.018) established0.018 (0.018, 0.018) established
textobjectCopy1 / 01 / 00.934 (0.872, 1.001)0.001 (0.001, 0.001) established0.001 (0.001, 0.001) established
textobjectCopyReuse2 / 02 / 00.994 (0.962, 1.026)0.001 (0.001, 0.001) established0.001 (0.001, 0.001) established
nesteddeserialize169 / 512172 / 5121.006 (0.950, 1.066)0.712 (0.690, 0.735) established0.717 (0.674, 0.763) established
nesteddeserializeReuse180 / 512179 / 5121.004 (0.958, 1.053)0.741 (0.709, 0.775) established0.745 (0.718, 0.772) established
nestedstreamCopy16 / 016 / 00.960 (0.934, 0.988)0.032 (0.031, 0.032) established0.030 (0.029, 0.031) established
nestedobjectCopy1 / 01 / 00.990 (0.910, 1.077)0.004 (0.003, 0.004) established0.004 (0.003, 0.004) established
nestedobjectCopyReuse2 / 02 / 00.988 (0.971, 1.006)0.006 (0.006, 0.006) established0.006 (0.005, 0.006) established
collectionsdeserialize1441 / 35871488 / 37921.047 (0.943, 1.164)0.882 (0.816, 0.954) established0.924 (0.836, 1.021) established
collectionsdeserializeReuse1487 / 37921472 / 37921.012 (0.984, 1.042)0.898 (0.876, 0.921) established0.909 (0.887, 0.932) established
collectionsstreamCopy27 / 025 / 00.913 (0.807, 1.034)0.009 (0.007, 0.010) established0.008 (0.008, 0.008) established
collectionsobjectCopy1 / 01 / 01.013 (0.870, 1.180)0.001 (0.000, 0.001) established0.001 (0.000, 0.001) established
collectionsobjectCopyReuse2 / 02 / 00.996 (0.944, 1.051)0.001 (0.001, 0.001) established0.001 (0.001, 0.001) established
largedeserialize20321 / 13205620237 / 1320561.000 (0.994, 1.006)1.255 (1.246, 1.263) missed1.255 (1.253, 1.257) missed
largedeserializeReuse20296 / 13205620268 / 1320561.001 (0.996, 1.006)1.254 (1.248, 1.259) missed1.256 (1.250, 1.261) missed
largestreamCopy2984 / 02957 / 00.994 (0.966, 1.024)0.113 (0.112, 0.114) established0.112 (0.109, 0.116) established
largeobjectCopy1 / 01 / 01.070 (0.978, 1.171)0.000 (0.000, 0.000) established0.000 (0.000, 0.000) established
largeobjectCopyReuse2 / 02 / 01.002 (0.993, 1.012)0.000 (0.000, 0.000) established0.000 (0.000, 0.000) established

Protobuf 3:

WorkloadOperationBaselineCandidateCandidate/baselineBaseline native/ChillCandidate native/Chill
scalardeserialize112 / 376109 / 3760.971 (0.946, 0.996)0.667 (0.662, 0.672) established0.648 (0.629, 0.667) established
scalardeserializeReuse117 / 376117 / 3760.995 (0.974, 1.015)0.669 (0.656, 0.681) established0.665 (0.654, 0.676) established
scalarstreamCopy15 / 014 / 00.980 (0.945, 1.016)0.040 (0.039, 0.040) established0.039 (0.037, 0.041) established
scalarobjectCopy1 / 01 / 01.059 (1.006, 1.115)0.005 (0.005, 0.006) established0.006 (0.005, 0.006) established
scalarobjectCopyReuse2 / 02 / 00.979 (0.948, 1.010)0.008 (0.008, 0.009) established0.008 (0.008, 0.008) established
textdeserialize1275 / 41281272 / 41280.998 (0.991, 1.005)0.814 (0.807, 0.820) established0.812 (0.809, 0.815) established
textdeserializeReuse1280 / 41281282 / 41281.001 (0.993, 1.008)0.812 (0.807, 0.816) established0.812 (0.806, 0.818) established
textstreamCopy50 / 051 / 01.217 (0.919, 1.611)0.011 (0.011, 0.011) established0.013 (0.010, 0.019) established
textobjectCopy1 / 01 / 01.048 (0.992, 1.107)0.000 (0.000, 0.000) established0.000 (0.000, 0.000) established
textobjectCopyReuse2 / 02 / 00.869 (0.713, 1.060)0.001 (0.001, 0.001) established0.001 (0.001, 0.001) established
nesteddeserialize175 / 536177 / 5361.007 (0.975, 1.039)0.732 (0.710, 0.754) established0.737 (0.719, 0.754) established
nesteddeserializeReuse184 / 536184 / 5361.003 (0.965, 1.042)0.751 (0.721, 0.782) established0.753 (0.736, 0.769) established
nestedstreamCopy16 / 015 / 00.954 (0.927, 0.982)0.030 (0.029, 0.031) established0.029 (0.028, 0.029) established
nestedobjectCopy1 / 01 / 01.109 (0.992, 1.241)0.003 (0.003, 0.004) established0.004 (0.004, 0.004) established
nestedobjectCopyReuse2 / 02 / 01.027 (0.997, 1.057)0.005 (0.005, 0.005) established0.006 (0.005, 0.006) established
collectionsdeserialize1473 / 38161440 / 36621.003 (0.929, 1.084)0.950 (0.918, 0.983) established0.953 (0.874, 1.038) established
collectionsdeserializeReuse1459 / 38161451 / 37391.018 (0.944, 1.097)0.939 (0.899, 0.980) established0.955 (0.882, 1.033) established
collectionsstreamCopy25 / 026 / 01.029 (0.976, 1.084)0.008 (0.008, 0.008) established0.008 (0.008, 0.009) established
collectionsobjectCopy1 / 01 / 00.947 (0.856, 1.048)0.001 (0.001, 0.001) established0.001 (0.000, 0.001) established
collectionsobjectCopyReuse2 / 02 / 00.974 (0.938, 1.011)0.001 (0.001, 0.001) established0.001 (0.001, 0.001) established
largedeserialize21106 / 13208021082 / 1320800.998 (0.987, 1.010)1.270 (1.255, 1.285) missed1.268 (1.259, 1.277) missed
largedeserializeReuse21095 / 13208021155 / 1320801.005 (0.995, 1.015)1.267 (1.254, 1.281) missed1.274 (1.268, 1.280) missed
largestreamCopy3020 / 02995 / 00.979 (0.961, 0.998)0.112 (0.111, 0.113) established0.110 (0.108, 0.112) established
largeobjectCopy1 / 01 / 00.981 (0.903, 1.066)0.000 (0.000, 0.000) established0.000 (0.000, 0.000) established
largeobjectCopyReuse2 / 02 / 01.002 (0.994, 1.010)0.000 (0.000, 0.000) established0.000 (0.000, 0.000) established

Possible regressions#

One of the 50 candidate-versus-baseline intervals outside the write operations, 25 per profile, lies entirely above 1: Protobuf 3 scalar objectCopy at 1.059 (1.006, 1.115), an operation that returns its input reference in about 1 ns. No write interval lies entirely above 1. As in the earlier reports, such small intervals on unchanged paths are recorded as run-to-run drift between sequential runs and are not attributed to the change.

Limitations#

The two variants of each profile ran sequentially on one VM, over about two and a half hours, so host drift can affect native times; the Chill lane controls part of it within each block. Five blocks per run give limited information about the distribution of ratios. The plan is not compared with the earlier reports, which used other VMs, images, or kernels. The corpus has no recursive types, unknown fields, repeated strings, string-valued maps, proto2 strings, or values above 64 nested values; those use the traversal or its added checks, which the unit tests check for correctness but these measurements do not time. The corpus and warm setup do not cover first-use costs, other Flink or JDK versions, or record shapes outside the five workloads. An earlier plan, issue112-sizing-1, measured an intermediate source before the review changes; its results are not retained, and this report does not use them.

Retained evidence#

The data inventory lists the four result archives, their checksums, and how to reconstruct each measured tree.