You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@spark.apache.org by do...@apache.org on 2026/03/20 12:31:30 UTC
(spark) branch master updated: [SPARK-56032][SQL] Support subexpression elimination in `FilterExec` whole-stage codegen
This is an automated email from the ASF dual-hosted git repository.
dongjoon 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 21d69c4f9098 [SPARK-56032][SQL] Support subexpression elimination in `FilterExec` whole-stage codegen
21d69c4f9098 is described below
commit 21d69c4f9098c2081b1bb22eb6be35112ec4809c
Author: yangjie01 <ya...@baidu.com>
AuthorDate: Fri Mar 20 05:31:11 2026 -0700
[SPARK-56032][SQL] Support subexpression elimination in `FilterExec` whole-stage codegen
### What changes were proposed in this pull request?
This PR adds subexpression elimination support to `FilterExec.doConsume` in the whole-stage codegen path.
The implementation follows the established CSE pattern used by `ProjectExec`, `HashAggregateExec`, and `AggregateCodegenSupport`:
1. Collect `otherPreds` (excluding `notNullPreds`, which are simple `IsNotNull` checks with no CSE value) and run `ctx.subexpressionEliminationForWholeStageCodegen` to discover common subexpressions.
2. Wrap `generatePredicateCode` inside `ctx.withSubExprEliminationExprs` so that `genCode` calls within can look up pre-computed subexpressions.
3. Emit the CSE pre-computation code via `ctx.evaluateSubExprEliminationState` before the predicate checks.
Key design decisions:
- **Only `otherPreds` participate in CSE** — `notNullPreds` are guaranteed to be `IsNotNull` checks (by FilterExec's constructor partitioning logic) with no CSE value; including them would interfere with equivalence analysis.
- **Pre-evaluate input variables before CSE analysis** — `FilterExec` sets `usedInputs = AttributeSet.empty` to defer input evaluation for short-circuit optimization. However, `subexpressionEliminationForWholeStageCodegen`'s internal `getLocalInputVariableValues` has a side effect: it clears `ctx.currentVars[i].code` for input variables referenced by common subexpressions. If `notNullPreds` reference the same input variables, `generatePredicateCode`'s `evaluateRequiredVariables` would [...]
- **CSE evaluation is placed before predicate short-circuit checks** — This is an intentional tradeoff: common subexpressions are evaluated unconditionally even if an earlier `notNull` check would have short-circuited. For expensive shared expressions (e.g., `from_json` appearing in 500 predicates), the benefit of evaluating once vs N times far outweighs the cost of losing short-circuit on the CSE portion. When there are no common subexpressions, `subExprsCode` is empty and this path [...]
### Why are the changes needed?
Support subexpression elimination in FilterExec whole-stage codegen, and with this fix, the `from_json` codegen (PR #48466) can be safely re-enabled in a follow-up PR.
### Does this PR introduce _any_ user-facing change?
No.
### How was this patch tested?
Added two new test cases in `WholeStageCodegenSuite`
### Was this patch authored or co-authored using generative AI tooling?
Generated-by: Claude Sonnet 4.6
Closes #54862 from LuciferYang/SPARK-56032.
Lead-authored-by: yangjie01 <ya...@baidu.com>
Co-authored-by: YangJie <ya...@baidu.com>
Signed-off-by: Dongjoon Hyun <do...@apache.org>
---
.../sql/execution/basicPhysicalOperators.scala | 31 ++++++++++++++--
.../sql/execution/WholeStageCodegenSuite.scala | 42 +++++++++++++++++++++-
2 files changed, 70 insertions(+), 3 deletions(-)
diff --git a/sql/core/src/main/scala/org/apache/spark/sql/execution/basicPhysicalOperators.scala b/sql/core/src/main/scala/org/apache/spark/sql/execution/basicPhysicalOperators.scala
index f2cbfd2154a4..49da71e2b0af 100644
--- a/sql/core/src/main/scala/org/apache/spark/sql/execution/basicPhysicalOperators.scala
+++ b/sql/core/src/main/scala/org/apache/spark/sql/execution/basicPhysicalOperators.scala
@@ -253,8 +253,33 @@ case class FilterExec(condition: Expression, child: SparkPlan)
override def doConsume(ctx: CodegenContext, input: Seq[ExprCode], row: ExprCode): String = {
val numOutput = metricTerm(ctx, "numOutputRows")
- val predicateCode = generatePredicateCode(
- ctx, child.output, input, output, notNullPreds, otherPreds, notNullAttributes)
+ // Apply CSE to otherPreds only (notNullPreds are simple IsNotNull checks with no CSE value).
+ val (inputVarsCode, subExprsCode, predicateCode) =
+ if (conf.subexpressionEliminationEnabled && otherPreds.nonEmpty) {
+ val boundOtherPreds = otherPreds.map(
+ BindReferences.bindReference(_, output))
+ // Pre-evaluate input variables before CSE analysis: CSE clears
+ // ctx.currentVars[i].code as a side effect, which causes "is not an rvalue"
+ // errors if notNullPreds reference the same inputs.
+ val otherPredInputAttrs = AttributeSet(otherPreds.flatMap(_.references))
+ val inputVarsEvalCode = evaluateRequiredVariables(
+ child.output, input, otherPredInputAttrs)
+
+ val subExprs = ctx.subexpressionEliminationForWholeStageCodegen(boundOtherPreds)
+ val predCode: String = {
+ var code = ""
+ ctx.withSubExprEliminationExprs(subExprs.states) {
+ code = generatePredicateCode(
+ ctx, child.output, input, output, notNullPreds, otherPreds, notNullAttributes)
+ Seq.empty
+ }
+ code
+ }
+ (inputVarsEvalCode, ctx.evaluateSubExprEliminationState(subExprs.states.values), predCode)
+ } else {
+ ("", "", generatePredicateCode(
+ ctx, child.output, input, output, notNullPreds, otherPreds, notNullAttributes))
+ }
// Reset the isNull to false for the not-null columns, then the followed operators could
// generate better code (remove dead branches).
@@ -268,6 +293,8 @@ case class FilterExec(condition: Expression, child: SparkPlan)
// Note: wrap in "do { } while (false);", so the generated checks can jump out with "continue;"
s"""
|do {
+ | $inputVarsCode
+ | $subExprsCode
| $predicateCode
| $numOutput.add(1);
| ${consume(ctx, resultVars)}
diff --git a/sql/core/src/test/scala/org/apache/spark/sql/execution/WholeStageCodegenSuite.scala b/sql/core/src/test/scala/org/apache/spark/sql/execution/WholeStageCodegenSuite.scala
index 6004f6f76394..08e0a92e516a 100644
--- a/sql/core/src/test/scala/org/apache/spark/sql/execution/WholeStageCodegenSuite.scala
+++ b/sql/core/src/test/scala/org/apache/spark/sql/execution/WholeStageCodegenSuite.scala
@@ -25,11 +25,12 @@ import org.apache.spark.sql.catalyst.expressions.codegen.{ByteCodeStats, CodeAnd
import org.apache.spark.sql.execution.adaptive.DisableAdaptiveExecutionSuite
import org.apache.spark.sql.execution.aggregate.{HashAggregateExec, SortAggregateExec}
import org.apache.spark.sql.execution.columnar.InMemoryTableScanExec
+import org.apache.spark.sql.execution.debug.codegenString
import org.apache.spark.sql.execution.joins.{BroadcastHashJoinExec, BroadcastNestedLoopJoinExec, ShuffledHashJoinExec, SortMergeJoinExec}
import org.apache.spark.sql.functions._
import org.apache.spark.sql.internal.SQLConf
import org.apache.spark.sql.test.SharedSparkSession
-import org.apache.spark.sql.types.{IntegerType, StringType, StructType}
+import org.apache.spark.sql.types.{IntegerType, StringType, StructField, StructType}
// Disable AQE because the WholeStageCodegenExec is added when running QueryStageExec
class WholeStageCodegenSuite extends QueryTest with SharedSparkSession
@@ -944,4 +945,43 @@ class WholeStageCodegenSuite extends QueryTest with SharedSparkSession
}
}
}
+
+ test("SPARK-56032: subexpression elimination in FilterExec codegen") {
+ val schema = StructType(Seq(
+ StructField("a", IntegerType, nullable = true),
+ StructField("b", IntegerType, nullable = true)))
+ val data = spark.sparkContext.parallelize(Seq(
+ Row(1, 5), Row(null, 3), Row(4, null), Row(5, 6), Row(7, 8), Row(2, 3)))
+
+ val expected = Seq(Row(1, 5), Row(5, 6), Row(2, 3))
+
+ Seq("1", Int.MaxValue.toString).foreach { splitThreshold =>
+ def testFilterCSE(cseEnabled: Boolean): String = {
+ withSQLConf(
+ SQLConf.SUBEXPRESSION_ELIMINATION_ENABLED.key -> cseEnabled.toString,
+ SQLConf.WHOLESTAGE_CODEGEN_ENABLED.key -> "true",
+ SQLConf.ADAPTIVE_EXECUTION_ENABLED.key -> "false",
+ SQLConf.CODEGEN_METHOD_SPLIT_THRESHOLD.key -> splitThreshold) {
+ val df = spark.createDataFrame(data, schema)
+ val filtered = df.where(
+ "a IS NOT NULL AND (a + b) > 3 AND (a + b) < 15 AND (a + b) != 10")
+ val plan = filtered.queryExecution.executedPlan
+ assert(plan.exists(_.isInstanceOf[WholeStageCodegenExec]),
+ "Filter should be in whole-stage codegen")
+ checkAnswer(filtered, expected)
+ codegenString(plan)
+ }
+ }
+
+ val cseCode = testFilterCSE(cseEnabled = true)
+ val noCseCode = testFilterCSE(cseEnabled = false)
+
+ val addExactPattern = "addExact".r
+ val cseAddCount = addExactPattern.findAllIn(cseCode).length
+ val noCseAddCount = addExactPattern.findAllIn(noCseCode).length
+ assert(cseAddCount < noCseAddCount,
+ s"CSE should reduce repeated evaluation (splitThreshold=$splitThreshold): " +
+ s"addExact appears $cseAddCount times with CSE vs $noCseAddCount times without")
+ }
+ }
}
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@spark.apache.org
For additional commands, e-mail: commits-help@spark.apache.org