[Java] Avoid RowCoder bytecode generation - #39619
Conversation
|
Assigning reviewers: R: @Abacn for label java. Note: If you would like to opt out of this review, comment Available commands:
The PR bot will only process comments in the main thread (not review comments). |
|
Thanks, the analysis and the change make sense to me, and the benchmark looks promising. However since it's a core code path, adding contributor who had context here cc: @reuvenlax (for previous changes #5723 #14591) |
|
And @stankiewicz |
|
Will take a look. Can you provide more context on the advantages? In particular, JVM startup time is probably dominated by other things and JVM startup time is usually a tiny fraction of pipeline runtime. Are there other advantages here, or is it just startup time? |
|
Another comment: the benchmarks should also test large schemas (> 1000 fields), and these should have nested fields as well. There are users that have some very large schemas, and we don't want to regress them. |
|
Thanks! I will look a bit deeper and implement some benchmarks with large schemas/nested fields, it makes sense! Will keep this as draft for now. But yes, the advantage ends up being a much faster creation (for every schema), and way less bookkeeping of the generated classes, as JVM retains class metadata for everything instantiated. At the steady state, after it's classloaded and JIT has ran, it shouldn't make much difference. (Also I think it simplifies the code considerably, which might be a win by itself.) |
RowCoderGeneratorcurrently creates and loads one ByteBuddy class for every uncached schema. Generatedencodeanddecodemethods only load instance fields and call existing Java delegates, so class generation adds startup time, allocation, metaspace pressure, and class-loader complexity without specializing field encoding.This replaces generated classes with one immutable
CustomCoder<Row>implementation while preserving schema UUID caching, encoding-position overrides, component coders, and existing encode/decode algorithms. It also adds JMH coverage for uncached coder creation and steady-state encoding/decoding.Benchmarks
JDK 17, same host, three isolated JVM forks. Generation includes schema construction;
buildSchemais a control.masterCommand:
./gradlew :sdks:java:core:jmh:jmh --args='org.apache.beam.sdk.jmh.coders.RowCoderGenerationBenchmark -prof gc -f 3 -foe=true'Steady-state results use identical benchmarks and three forks:
masterns/opAll old/new confidence intervals overlap, showing no material steady-state regression.
Historical context
RowCoder generation was introduced in 2018 to replace an implementation that inspected the schema and rebuilt component coders while processing every row. The combined change was reported as 30-40% faster. That comparison was against the former introspecting implementation, not against an ordinary coder with the same precomputed fields used here.
Over time, component-coder construction moved into regular Java and the resulting array was passed into the generated class. Its encode and decode methods remained forwarding wrappers around shared loops. Precomputing field coders is still valuable and remains unchanged in this PR; generating a class no longer provides that benefit. Current benchmarks show equivalent steady-state performance, while class generation adds substantial creation time and allocation.
Wire compatibility and testing
Encoding and decoding algorithms are unchanged. Java passes Beam's exact standard-coder vectors, including
beam:coder:row:v1vectors produced by Python../gradlew :runners:java-fn-execution:test --tests org.apache.beam.runners.fnexecution.wire.CommonCoderTest(227 passed)./gradlew :sdks:java:core:test(full core suite passed;RowCoderTest28/28 andSchemaCoderTest19/19)./gradlew :sdks:java:core:spotlessApply :sdks:java:core:jmh:spotlessApplyRowCoderBenchmarkJMH runs for dynamic and static encodingsCHANGES.mdentry; internal performance change with unchanged API and wire format.