This is an automated email from the ASF dual-hosted git repository.
gengliangwang pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/spark.git
The following commit(s) were added to refs/heads/master by this push:
new c116a27e1042 [SPARK-57902][SQL] Emit a ternary for 2-arg Coalesce with
a non-nullable pure fallback
c116a27e1042 is described below
commit c116a27e104249c0a048831e2bbe0e4ca76ca050
Author: Gengliang Wang <[email protected]>
AuthorDate: Mon Jul 6 12:39:50 2026 -0700
[SPARK-57902][SQL] Emit a ternary for 2-arg Coalesce with a non-nullable
pure fallback
### What changes were proposed in this pull request?
The general `Coalesce` codegen evaluates its children inside a `do { ... }
while (false)` loop and,
before that, promotes the result's null flag to a **class-level mutable
field** via
`ctx.addMutableState`:
```scala
ev.isNull =
JavaCode.isNullGlobal(ctx.addMutableState(CodeGenerator.JAVA_BOOLEAN,
ev.isNull))
```
It has to be a field rather than a local `boolean` because the branches may
be split out into a
separate `coalesce_N(...)` method (via
`splitExpressionsWithCurrentInputs`), so the flag must be
visible across that method boundary. So every coalesce on the general path
adds one
`private boolean ...;` field to the generated class, e.g. for `coalesce(a,
b)`:
```java
private boolean coalesce_isNull_0; // added to the generated class
...
coalesce_isNull_0 = true;
int coalesce_value_0 = -1;
do {
if (!aIsNull) { coalesce_isNull_0 = false; coalesce_value_0 = aValue;
continue; }
if (!bIsNull) { coalesce_isNull_0 = false; coalesce_value_0 = bValue;
continue; }
} while (false);
```
For `coalesce(a, b)` where `b` is non-nullable and its evaluation emits
**no** code (a literal or an
already-evaluated value) -- e.g. the `coalesce(sum, 0)` in every SUM/AVG
update -- the result can
never be null and `b` carries a pure Java expression, so the whole thing
collapses to a single
ternary with **no** field and **no** loop (`isNull` becomes the
compile-time constant
`FalseLiteral`):
```java
int coalesce_value_0 = aIsNull ? 0 : aValue;
```
The empty-code requirement preserves lazy evaluation: there are no
statements of `b` to hoist past
the null check. When the fast path does not apply, the general path reuses
the `ExprCode` already
generated while probing (generating a child twice would duplicate codegen
side effects on the
context, such as orphaned mutable state for stateful expressions and
repeated reference
registrations).
Note on why this shrinks the compiled artifact and not just the source: the
generated code is
compiled by Janino, which does no dead-code elimination or field pruning,
so the removed
`do/while` scaffold and the `private boolean` field (which also occupies a
constant-pool entry and
per-instance state) genuinely disappear from the generated class -- they
are not something a
compiler would have optimized away. Spark also intentionally bounds the
number of mutable states,
which is what the `SPARK-22705` test guards.
### Why are the changes needed?
This is part of the effort to reduce generated Java size in whole-stage
codegen (parent
[SPARK-56908](https://issues.apache.org/jira/browse/SPARK-56908)). Smaller
generated methods reduce
Janino compilation work, JIT work, and pressure against the JVM 64KB method
limit. Measured on a
TPC-DS codegen dump (150 queries, 1,572 whole-stage-codegen subtrees):
about **-2%** of total
generated source (109/150 queries smaller, 0 larger), and one global
mutable field removed per
affected coalesce.
### Does this PR introduce _any_ user-facing change?
No. This only changes the shape of generated code; results are unchanged.
### How was this patch tested?
`NullExpressionsSuite` -- added a test asserting the two-argument
non-nullable-pure-fallback form
compiles to a ternary with no global mutable state and evaluates correctly
on both the null and
non-null branches. The existing `SPARK-22705` global-variable test is
updated to use three children
so it continues to exercise the general do-while path (the two-argument
form it previously used now
takes the new fast path).
### Was this patch authored or co-authored using generative AI tooling?
Generated-by: Claude Code (Opus 4.8)
Closes #56971 from gengliangwang/SPARK-57902-coalesce-ternary.
Authored-by: Gengliang Wang <[email protected]>
Signed-off-by: Gengliang Wang <[email protected]>
---
.../sql/catalyst/expressions/nullExpressions.scala | 31 ++++++++++++++++++--
.../expressions/NullExpressionsSuite.scala | 34 +++++++++++++++++++++-
2 files changed, 61 insertions(+), 4 deletions(-)
diff --git
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/nullExpressions.scala
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/nullExpressions.scala
index e7be588c4b46..1d50d8987cce 100644
---
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/nullExpressions.scala
+++
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/nullExpressions.scala
@@ -93,11 +93,37 @@ case class Coalesce(children: Seq[Expression])
}
override def doGenCode(ctx: CodegenContext, ev: ExprCode): ExprCode = {
+ val resultType = CodeGenerator.javaType(dataType)
+ // Fast path for the common `coalesce(a, b)` where `b` is non-nullable and
its evaluation
+ // generates no code (a literal or an already-evaluated value), e.g. the
`coalesce(sum, 0)`
+ // emitted for every SUM/AVG update. The result can never be null and `b`
carries a pure
+ // Java expression, so a single ternary replaces the general do-while
block below and no
+ // global mutable isNull state is needed. The empty-code requirement also
preserves lazy
+ // evaluation: there are no statements of `b` to hoist before the null
check.
+ val probedEvals: Option[Seq[ExprCode]] =
+ if (children.length == 2 && !children(1).nullable) {
+ val first = children.head.genCode(ctx)
+ val second = children(1).genCode(ctx)
+ if (second.code.isEmpty) {
+ return ev.copy(
+ code = code"""
+ |${first.code}
+ |$resultType ${ev.value} = ${first.isNull} ? ${second.value} :
${first.value};
+ """.stripMargin,
+ isNull = FalseLiteral)
+ }
+ // The fast path does not apply, so the general path below reuses the
code generated for
+ // the probe: generating a child twice would duplicate codegen side
effects on the context
+ // (orphaned mutable state for stateful expressions, repeated
reference registrations).
+ Some(Seq(first, second))
+ } else {
+ None
+ }
+
ev.isNull =
JavaCode.isNullGlobal(ctx.addMutableState(CodeGenerator.JAVA_BOOLEAN,
ev.isNull))
// all the evals are meant to be in a do { ... } while (false); loop
- val evals = children.map { e =>
- val eval = e.genCode(ctx)
+ val evals = probedEvals.getOrElse(children.map(_.genCode(ctx))).map { eval
=>
s"""
|${eval.code}
|if (!${eval.isNull}) {
@@ -108,7 +134,6 @@ case class Coalesce(children: Seq[Expression])
""".stripMargin
}
- val resultType = CodeGenerator.javaType(dataType)
val codes = ctx.splitExpressionsWithCurrentInputs(
expressions = evals,
funcName = "coalesce",
diff --git
a/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/NullExpressionsSuite.scala
b/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/NullExpressionsSuite.scala
index 5c19e69cdfa3..3d957b662d5d 100644
---
a/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/NullExpressionsSuite.scala
+++
b/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/NullExpressionsSuite.scala
@@ -21,6 +21,7 @@ import java.sql.Timestamp
import org.apache.spark.{SparkFunSuite, SparkRuntimeException}
import org.apache.spark.sql.catalyst.FunctionIdentifier
+import org.apache.spark.sql.catalyst.InternalRow
import org.apache.spark.sql.catalyst.analysis.{FunctionRegistry,
SimpleAnalyzer, UnresolvedAttribute}
import org.apache.spark.sql.catalyst.expressions.codegen.CodegenContext
import org.apache.spark.sql.catalyst.expressions.objects.AssertNotNull
@@ -207,10 +208,41 @@ class NullExpressionsSuite extends SparkFunSuite with
ExpressionEvalHelper {
test("SPARK-22705: Coalesce should use less global variables") {
val ctx = new CodegenContext()
- Coalesce(Seq(Literal("a"), Literal("b"))).genCode(ctx)
+ // Use three children so that the general do-while codegen path is
exercised: the two-argument
+ // form with a non-nullable pure fallback is now special-cased to a
ternary with no global
+ // state (covered by the test below).
+ Coalesce(Seq(Literal("a"), Literal("b"), Literal("c"))).genCode(ctx)
assert(ctx.inlinedMutableStates.size == 1)
}
+ test("Coalesce with a non-nullable pure fallback compiles to a ternary with
no global state") {
+ val ctx = new CodegenContext()
+ Coalesce(Seq(Literal.create(null, StringType), Literal("b"))).genCode(ctx)
+ assert(ctx.inlinedMutableStates.isEmpty)
+ checkEvaluation(Coalesce(Seq(Literal.create(null, StringType),
Literal("a"))), "a")
+ checkEvaluation(Coalesce(Seq(Literal("x"), Literal("y"))), "x")
+ }
+
+ test("Coalesce ternary fast path over a BoundReference first arg and a
primitive fallback") {
+ // The motivating hot path is coalesce(nullable_column, literal): the
first arg is a
+ // BoundReference read from the input row (a distinct codegen path from a
Literal), and the
+ // fallback is a non-nullable primitive that emits no code, so the ternary
fast path applies.
+ val coalesce = Coalesce(Seq(BoundReference(0, IntegerType, nullable =
true), Literal(0)))
+ checkEvaluation(coalesce, 42, InternalRow(42))
+ checkEvaluation(coalesce, 0, InternalRow(null))
+ }
+
+ test("Coalesce reuses the probed ExprCode when the non-nullable fallback
emits code") {
+ // A two-arg coalesce whose non-nullable fallback is itself a
BoundReference: its code is
+ // non-empty, so the ternary fast path does not apply and the probed
ExprCodes are threaded
+ // into the general do-while path (rather than generating the children a
second time).
+ val coalesce = Coalesce(Seq(
+ BoundReference(0, IntegerType, nullable = true),
+ BoundReference(1, IntegerType, nullable = false)))
+ checkEvaluation(coalesce, 42, InternalRow(42, 7))
+ checkEvaluation(coalesce, 7, InternalRow(null, 7))
+ }
+
test("AtLeastNNonNulls should not throw 64KiB exception") {
val inputs = (1 to 4000).map(x => Literal(s"x_$x"))
checkEvaluation(AtLeastNNonNulls(1, inputs), true)
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]