perf: optimize JVM columnar-to-row conversion - #5496
Draft
peterxcli wants to merge 1 commit into
Draft
Conversation
peterxcli
marked this pull request as draft
August 27, 2026 15:37
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.
Which issue does this PR close?
Closes #5119.
This completes the two implementation tasks retained in that issue. It refreshes and productionizes the prototype from #5120 on current
main, with expanded correctness coverage and a new same-machine A/B benchmark.Rationale for this change
The interpreted JVM columnar-to-row path used by
CometColumnarToRowExecreads every value throughColumnVectoraccessors and then writes it throughUnsafeProjection. That path is already efficient for simple primitive schemas, but it creates substantial per-value garbage for decimals:Decimalobject per value;byte[]/BigInteger/BigDecimalchain;The benchmark introduced in #5113 originally covered only
long, int, double, string, which is close to the existing JVM path's best case. The broader schema analysis in #5112 and #5118 showed that decimal-heavy and all-fixed-width schemas behave very differently. This PR therefore expands the benchmark matrix and adds a direct JVM converter that writes Arrow values into Spark'sUnsafeRowrepresentation while reusing its row and buffers.This approach also avoids crossing JNI and avoids the native converter's per-row defensive copies required by the row-lifetime contract fixed in #3367.
Related behavior and history were cross-checked against #5114, #5115, #3308, #3221, #3266, #3268, #3649, and the production GC discussion in #4440.
What changes are included in this PR?
Direct JVM converter
Adds
DirectColumnarToRowConverter, which resolves column types once and writes directly intoUnsafeRowstorage.Supported types are:
The converter uses:
The output is byte-identical to
UnsafeProjection, including null-slot zeroing, deterministic padding, multi-word null bitsets, wide-decimal reservations, and Spark's canonical NaN representation. The converter also rejects oversized schemas, batches, and row buffers before allocation.Integration and fallbacks
Regular execution and broadcast relation builds now share one conversion helper.
Two experimental settings control the feature:
spark.comet.exec.columnarToRow.direct.enabled=falsespark.comet.exec.columnarToRow.direct.minBatchSize=128The optimization is disabled by default. When enabled:
rowIteratorplusUnsafeProjectiononce per plan;minBatchSizefall back independently;The settings and fallback behavior are documented in the tuning guide.
Benchmark coverage
CometC2RIsolatedBenchnow compares the existing JVM path, the direct converter, and the native converter across five representative schemas and batch sizes 8192, 512, and 32. It reports both wall-clock time and JVM heap allocation per row. The sink reads rawUnsafeRowslots so benchmark-side wrapper allocation does not distort converter cost.Known trade-off
The direct path is not universally faster. The mixed primitive/string schema contains no expensive decimal accessors, and its string column prevents the fixed-width path from engaging.
UnsafeProjectionis already schema-specialized straight-line code for this case, while the general direct path still pays per-field dispatch and row-assembly costs.For that reason this PR keeps the feature opt-in. It does not add a benchmark-tuned schema heuristic, and the small-batch threshold only addresses per-batch amortization rather than the large-batch mixed-schema regression.
How are these changes tested?
Correctness and repository checks
DirectColumnarToRowConverterSuitecompares raw output bytes withUnsafeProjectionacross supported types, nulls, fixed- and variable-width paths, decimal boundaries, empty strings, multi-word null bitsets, noncanonical float/double NaN payloads, and oversized fixed-width batches.CometDirectColumnarToRowSuiteverifies end-to-end Spark results and plan selection with whole-stage codegen disabled, including mixed types, the fixed-width path, unsupported-schema fallback, and minimum-batch fallback.Commands run after rebasing onto current
main:make core ./mvnw test -Dtest=none \ -Dsuites=org.apache.comet.DirectColumnarToRowConverterSuite,org.apache.comet.exec.CometDirectColumnarToRowSuite make format PROFILES=-Pspark-4.0 python3 dev/ci/check-suites.py git diff --check upstream/main...HEADResults:
Benchmark
Command:
Environment:
-Xmx20g;-Ctarget-cpu=native.Protocol:
mainand the proposed converter;batchSize=8192.The A/B was recorded at the implementation base (
2699f59b7). The subsequent rebase did not changeCometColumnarToRowExec, the benchmark, or the vector accessors onmain; the current rebased code was recompiled and retested as listed above.long, int, double, string4 x decimal(12,2), date, 2 x string4 x decimal(12,2), date, long2 x decimal(38,10), long16 x longThe direct converter wins 4 of 5 representative large-batch schemas. Winning cases improve by 1.12-1.93x, while decimal-heavy schemas reduce JVM heap allocation by 84-97%. The mixed primitive/string regression is intentionally reported rather than averaged away and is why the feature remains disabled by default.