txwyy123 opened a new pull request, #59841:
URL: https://github.com/apache/spark/pull/59841

   ### What changes were proposed in this pull request?
   
   For SortMergeJoin with many output columns (80+), `processNext()` contains 
all column extraction code inline — reading every column from both the streamed 
and buffered rows. This pushes `processNext()` past the 8,000-byte JVM bytecode 
limit where HotSpot C2 refuses to JIT-compile, causing the method to run 
interpreted at ~4x penalty.
   
   This PR adds **field-based extraction** for both sides of a SortMergeJoin:
   
   1. **`genOneSideJoinVarsAsFields`** (buffered side in `JoinCodegenSupport`): 
stores extracted column values in class-level mutable fields 
(`ctx.addMutableState`) instead of local variables, then uses 
`ctx.splitExpressions` to batch extraction code into small helper methods that 
take `InternalRow bufferedRow` as a parameter.
   
   2. **`createStreamedVarsAsFields`** (streamed side in `SortMergeJoinExec`): 
same pattern but with **no arguments** to `splitExpressions`, since 
`streamedRow` is already a class field from `genScanner`. The helper methods 
reference it directly.
   
   For a 40+40 column join, `processNext` shrinks from 10,221 to 5,766 source 
chars (**43% reduction**), with column access operations dropping from 204 to 1 
inside `processNext`:
   
   | Metric | Before | After |
   |---|---|---|
   | processNext lines | 313 | 197 |
   | processNext source chars | 10,221 | 5,766 |
   | Column access ops in processNext | 204 | 1 |
   | Extraction helper methods | 0 | 6 |
   
   ### Why are the changes needed?
   
   The JVM's C2 JIT compiler refuses to compile methods with ≥ 8,000 bytes of 
bytecode ([the 8K cliff](https://vecbricks.github.io/the-8000-byte-cliff/)). 
When `processNext()` exceeds this limit on wide joins, it silently falls back 
to the bytecode interpreter. Spark's `maxFields=100` default masks this by 
disabling codegen entirely for wide schemas, but users who raise `maxFields` to 
enable codegen on wider queries hit the cliff.
   
   Distributed EMR benchmarks (10× r5.4xlarge, 5M rows/side, Spark 3.5.2) 
confirmed the problem:
   
   | Output columns | Codegen ON | Codegen OFF | Delta |
   |---|---|---|---|
   | 42 | 2,715 ms | 2,750 ms | **+1%** ✅ processNext < 8K |
   | 62 | 3,403 ms | 3,460 ms | **+2%** ✅ processNext < 8K |
   | 102 | 5,173 ms | 4,896 ms | **-5%** ❌ processNext > 8K |
   | 122 | 5,948 ms | 5,487 ms | **-8%** ❌ interpreted codegen WORSE |
   
   At 100+ output columns, codegen ON is **5–8% slower** than codegen OFF 
because interpreted codegen is worse than the Volcano iterator model (the 
pre-compiled `Row.getInt(i)` accessors are JIT'd, while the 
dynamically-generated Janino bytecode is not).
   
   ### Does this PR introduce _any_ user-facing change?
   
   No. This is a performance optimization. Query results are unchanged.
   
   ### How was this patch tested?
   
   - New tests in `WholeStageCodegenSuite` for wide SMJ (40+40 cols) and BHJ 
(40+40 cols) with `maxFields=200`
   - Full existing test suites pass: WholeStageCodegenSuite (99), JoinSuite 
(59), OuterJoinSuite (96), InnerJoinSuite + ExistenceJoinSuite (514)
   - `WideJoinBenchmark` measuring `processNext` method size before/after
   - Distributed EMR benchmark (10× r5.4xlarge, Spark 3.5.2) measuring 
wall-clock query time across 7 column widths
   
   ### Was this patch authored or co-authored using generative AI tooling?
   
   Yes.
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to