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

   ### What changes were proposed in this pull request?
   
   This replaces #59069. It is a redesign that takes in the reviews by 
@dongjoon-hyun and @cloud-fan there.
   
   Under whole-stage codegen, 
`CodegenContext.splitExpressionsWithCurrentInputs` never split (`// TODO: 
support whole stage codegen`), so every branch of a `CASE WHEN` went into the 
operator's method. Outside a stage the branches are split into methods that 
take the input row. Inside a stage the inputs are local variables of the 
operator's method, and which of them a piece of code reads cannot be told from 
the code strings. This PR splits a `CASE WHEN` inside a stage, and only when 
the stage needs it.
   
   **One input collector.** A private `CodegenContext.collectInputs` walks the 
expressions a piece of code was generated from and returns the locals it reads, 
or `None` when no method can take them. An `InputPolicy` records where its 
three callers differ: the whole-stage split, `getLocalInputVariableValues` 
(subexpressions, aggregates, `ExpandExec`), and `CommonExprSlots.methodArgs` 
(`With`). It replaces the two walks that existed before and the third that 
#59069 added. Under the whole-stage policy, the walk:
   - takes the evaluated input variables of `currentVars`;
   - takes `INPUT_ROW` only where the code reads it, including through a 
`CommonExpressionRef` whose definition became a method taking the row (that 
decision is now recorded when it is made);
   - takes a subexpression state's value without descending below it;
   - reads a slot of a compacted mutable state array as a field.
   
   It refuses an input the operator has not evaluated yet, and a value no 
parameter can carry. `getLocalInputVariableValues` keeps its signature and its 
results.
   
   **One split, with a per-block fallback.** `CaseWhen` passes each branch's 
code together with the expressions it came from to a new `private[sql] 
splitExpressionsWithSources`. The existing `splitExpressionsWithCurrentInputs` 
keeps its signature for external callers.
   - The code is packed into blocks by `methodSplitThreshold`, as everywhere, 
and each block gets its own arguments.
   - A block the collector refuses stays inline, in its place between the calls.
   - Only when every block is refused does the `CASE WHEN` stay in one piece.
   
   **One grouping.** The calls are grouped by the grouping of SPARK-59783 
(#59042), now the only implementation, with a parameter for how many calls may 
stay in the calling method. Inside a stage that is 8 calls. All of a 
projection's outputs land in one method there, so a per-expression budget would 
add up: four 300-branch `CASE WHEN`s gave a 13583-byte method that HotSpot 
never compiles.
   
   **The stage gate.** Splitting where the stage did not need it cost 5-15% at 
64 branches on the runners: C2 compiles the unsplit method as one unit, but 
stops inlining partway through the chain of calls. So a stage now decides from 
its own code:
   1. It generates its code without the split, recording which expressions it 
could split.
   2. It compiles that code.
   3. If the compile fails, or a method is past 
`spark.sql.codegen.wholeStage.splitExpressions.methodLimit` (8000 bytes by 
default, HotSpot's limit for JIT-compiling a method), it generates the code 
again with the split.
   4. It keeps the split code only when that makes the stage's largest method 
smaller and it compiles. Otherwise it runs the unsplit code.
   
   A stage the JIT compiles whole therefore keeps exactly the code it had 
before this PR. The details:
   - The trial compile holds back its warnings and the huge-method report for 
the code it discards, also on the JDK compiler's worker thread.
   - A failed trial is remembered by a digest of the code, so re-executing a 
query does not retry a 64 KB compile.
   - The compile of the code that runs is cached as before.
   
   **The discarded pass.** Subexpression elimination generates the common 
subexpressions once without splitting, and again inside `subExpr_N` methods 
when they are large. The functions added by the discarded first generation are 
now removed from the class.
   
   `spark.sql.codegen.wholeStage.splitExpressions` (internal, on by default) 
turns all of this off. With it off, the generated code is exactly that of 
master.
   
   ### Why are the changes needed?
   
   SPARK-33301: a large `CASE WHEN` inside a stage fails to compile. On Spark 
4.2.0, one `CASE WHEN` of `n` branches over a long column,
   
   ```scala
   val branches = (1 to n).map(k => s"WHEN id = $k THEN id * $k").mkString(" ")
   spark.sql(s"SELECT CASE $branches ELSE 0 END AS v FROM range(10)").collect()
   ```
   
   compiles at 952 branches and fails at 953 ("Code grows beyond 64 KB"). Spark 
then disables whole-stage codegen for the plan and runs it without. Much 
earlier, from 96 branches in the benchmark's shape, the stage's method is past 
8000 bytes, and HotSpot never JIT-compiles it (`-XX:+DontCompileHugeMethods`), 
so the stage runs interpreted.
   
   `CaseWhenCodegenBenchmark` (new), best times on GitHub runners, JDK 17 / 21 
/ 25, at `42bbae0c829`. The results files of this PR's head are being generated 
now and will be committed here.
   
   | shape | split as needed | not split | whole stage codegen off |
   |---|---|---|---|
   | one CASE WHEN, 64 branches (6049 bytes) | 55 / 101 / 96 ms | 51 / 96 / 93 
ms | 113 / 228 / 217 ms |
   | one CASE WHEN, 96 branches (9057 bytes unsplit) | 70 / 136 / 126 ms | 1796 
/ 3497 / 3966 ms | 152 / 284 / 298 ms |
   | one CASE WHEN, 300 branches | 147 / 249 / 233 ms | 5752 / 10715 / 11973 ms 
| 644 / 930 / 974 ms |
   | a 300-branch CASE WHEN in a keyed aggregate | 181 / 308 / 276 ms | 5504 / 
12218 / 12141 ms | 1605 / 2350 / 2239 ms |
   
   Below 8000 bytes, "split as needed" runs the same code as "not split". The 
difference there is within each rung's A/A spread ("not split" against "not 
split again" in the files). Past it, the split is 25 to 51 times faster than 
the unsplit stage and 2 to 9 times faster than running without whole-stage 
codegen.
   
   Four 300-branch `CASE WHEN`s in one projection ran 44-62% slower split as 
needed than split always on those runs. The unsplit code fails to compile 
there, and that commit retried the failed trial compile on every execution. The 
digest memo in `484b4146d1b` removes the retry: on a laptop, a repeated plan 
and compile of a 1000-branch stage went from 479 ms to 129 ms. The runs on the 
head will show how much of the gap is left.
   
   ### Does this PR introduce _any_ user-facing change?
   
   No, apart from the performance of such queries. Answers are the same. A 
large `CASE WHEN` inside a stage now compiles, and keeps the stage's methods 
under the JIT limit. The exceptions are blocks that read a column the operator 
has not evaluated yet: they stay inline, and a `CASE WHEN` all of whose blocks 
do so stays in one piece, as today. A stage the JIT already compiles whole is 
unchanged.
   
   ### How was this patch tested?
   
   New tests in `WholeStageCodegenSuite` (23) and `CollectInputsSuite` (10, one 
per row of the collector's policies). Among them:
   - **Splitting:** a 3000-branch `CASE WHEN` compiles inside a stage, and at 
300 branches every method stays under 8000 bytes. Several large `CASE WHEN`s in 
one projection keep its method small.
   - **The gate:** a 64-branch stage keeps its unsplit code. The trial compile 
reports nothing for the code it discards, and the kept code reports its huge 
method. `chooseCode`'s decisions are checked with the compile faked, including 
a split that fails to compile.
   - **The per-block fallback:** a block reading an input not evaluated yet 
stays inline between the calls. A block whose method would take too many 
parameters stays inline too.
   - **Inputs:** keyed aggregates, the buffer row and `max_by`. Sort-merge and 
broadcast join conditions, with a common expression. The streamed-only 
condition of an outer join, which a `CASE WHEN` never reaches. A `ROLLUP`, and 
a split common subexpression. A slot of a compacted state array, read as a 
field.
   - **The discarded pass:** its functions are removed from the class.
   
   Existing suites pass: `CodeGenerationSuite`, `ConditionalExpressionSuite`, 
`SubexpressionEliminationSuite`, `WithExpressionEvalSuite`, 
`WholeStageCodegenSuite`, the TPC-DS and TPC-H query suites, the join suites 
and `DataFrameAggregateSuite`. Generated code for every TPC-DS query is 
unchanged by the collector. The fork's full CI passed on `484b4146d1b`.
   
   A differential fuzz, not part of the PR, compared random queries under 
whole-stage codegen with the same queries without it. The table has nullable 
numeric, string, date, decimal, array, struct and boolean columns, and the 
queries cover ten operator shapes with nested `CASE WHEN`s in conditions and 
values. It ran 72,000 queries over the method limit at 0, 1500 and the default, 
both compilers, `splitConsumeFuncByOperator` on and off, and three seeds.
   
   There was one difference, and it is not this PR's. With 
`methodSplitThreshold=1`, a sort-merge join key holding a `coalesce` trips 
SPARK-22668's testing assertion ("split function argument smj_streamedRow_0 
cannot be a global variable"). This happens on master too: 
`SortMergeJoinExec.createJoinKey` sets `INPUT_ROW` to the streamed row, which 
is a field, and `Coalesce` splits through `splitExpressionsWithCurrentInputs`. 
With this PR's split off, the same seed gives 32 differences, each a compile 
failure past 64 KB.
   
   Follow-ups, each for its own JIRA:
   - the other callers of `splitExpressionsWithCurrentInputs` (`Coalesce`, 
`In`, `Greatest`/`Least`, `CreateNamedStruct`, the hashes and others), passing 
their sources;
   - `Expression.reduceCodeSize`'s whole-stage TODO;
   - letting `getLocalInputVariableValues` follow `With`;
   - the join key assertion above.
   
   ### Was this patch authored or co-authored using generative AI tooling?
   
   Generated-by: Claude Code (Claude Opus 5.5)
   


-- 
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