mbutrovich commented on code in PR #6715:
URL: https://github.com/apache/datafusion-comet/pull/6715#discussion_r4221802584


##########
spark/src/main/scala/org/apache/comet/serde/CometScalaUDF.scala:
##########
@@ -58,6 +58,63 @@ object CometScalaUDF extends CometExpressionSerde[ScalaUDF] {
   override def convert(expr: ScalaUDF, inputs: Seq[Attribute], binding: 
Boolean): Option[Expr] =
     emitJvmCodegenDispatch(expr, inputs, binding)
 
+  /**
+   * Emits Spark's null guard around a Scala UDF, together with the UDF, as 
one dispatcher call.
+   * Returns `None` when `expr` is not that guard, when an expression in it is 
disabled, or when
+   * the dispatcher cannot take it, and records nothing on `expr` in that 
case: the caller
+   * converts `expr` as usual, which sends the UDF to the dispatcher on its 
own, and the UDF
+   * records its own fallback reason if it cannot go.
+   *
+   * Spark's `HandleNullInputsForUDF` rule wraps a UDF with a primitive 
parameter over a nullable
+   * input as `if (isnull(a) or isnull(b)) null else f(knownnotnull(a), 
knownnotnull(b))`, one
+   * `IsNull` per such argument. Converted natively, the `If` is a `CASE` that 
splits the batch by
+   * the predicate and merges the two branches back together, which costs more 
than the call it
+   * protects. In the kernel it is a branch per row.
+   *
+   * Only that shape matches: the predicate must be the one the rule builds 
from the UDF's guarded
+   * arguments. The kernel then runs no expression it would not run for the 
UDF alone, apart from
+   * the guard's own `if`, `or` and `isnull`, and it runs Spark's code for all 
of them. In a
+   * filter or join condition, and in the predicate of a conditional,
+   * `ReplaceNullWithFalseInPredicate` replaces the null with `false`, so that 
form matches too.
+   */
+  def emitNullGuardDispatch(expr: If, inputs: Seq[Attribute], binding: 
Boolean): Option[Expr] =
+    if (isNullGuard(expr) && isEnabled(expr)) {
+      tryJvmCodegenDispatch(expr, inputs, binding).toOption
+    } else {
+      None
+    }
+
+  /**
+   * Whether converting `guard` natively would pass every 
`spark.comet.expression.<name>.enabled`
+   * check it makes: on the UDF, and on each node of the predicate and the 
true branch, the
+   * guarded arguments included. The dispatcher makes none of these checks, so 
a guard with a
+   * disabled expression stays native, where the conversion records the 
fallback reason. The UDF's
+   * arguments are not checked, because the dispatcher takes them unchecked 
with or without the
+   * guard.
+   */
+  private def isEnabled(guard: If): Boolean = {
+    def disabled(e: Expression): Boolean =
+      QueryPlanSerde.exprSerdeMap.get(e.getClass).exists { serde =>
+        !CometConf.isExprEnabled(
+          
serde.asInstanceOf[CometExpressionSerde[Expression]].getExprConfigName(e))
+      }
+    !disabled(guard.falseValue) && !guard.predicate.exists(disabled) &&
+    !guard.trueValue.exists(disabled)
+  }
+
+  private def isNullGuard(expr: If): Boolean = expr match {
+    case If(predicate, Literal(null, _) | Literal.FalseLiteral, udf: ScalaUDF) 
=>
+      val guarded = udf.inputPrimitives.zip(udf.children).collect {
+        case (true, KnownNotNull(arg)) => arg
+      }
+      // `semanticEquals` is false for a nondeterministic argument, so that 
guard stays native.
+      guarded
+        .map(IsNull(_))
+        .reduceLeftOption[Expression](Or)
+        .exists(_.semanticEquals(predicate))

Review Comment:
   What do you think about also matching a UDF that gets the same column at 
more than one primitive position? 
[`HandleNullInputsForUDF`](https://github.com/apache/spark/blob/e221b56be7b6d9e48e107fc4d1cf0c15f02700f8/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/analysis/Analyzer.scala#L3625-L3660)
 adds one `isnull` per position, and 
[`BooleanSimplification`](https://github.com/apache/spark/blob/e221b56be7b6d9e48e107fc4d1cf0c15f02700f8/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/optimizer/expressions.scala#L385)
 then drops the repeats. With `addBoth = (a: Long, b: Long) => a + b` on Spark 
4.1, `SELECT addBoth(a, a) FROM t` optimizes to `if (isnull(a)) null else 
addBoth(knownnotnull(a), knownnotnull(a))`, and `addThree(a, b, a)` optimizes 
to `if ((isnull(a) OR isnull(b))) null else addThree(...)`. The predicate 
rebuilt here keeps every repeat, so neither one matches, and extended explain 
lists `if` and `isnull` as native for both.
   
   Removing repeated arguments before building the predicate would cover these. 
[`ExpressionSet.add`](https://github.com/apache/spark/blob/e221b56be7b6d9e48e107fc4d1cf0c15f02700f8/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/ExpressionSet.scala#L97-L104)
 keeps the first of each group of semantically equal deterministic expressions 
and keeps every nondeterministic one, so a nondeterministic argument still 
fails `semanticEquals` and its guard stays native. This also needs 
`ExpressionSet` in the import on line 25.
   
   Could the first test also cover `addBoth(a, a)` and `addThree(a, b, a)`? A 
case where one primitive argument isn't nullable, such as `addBoth(a, 1L)`, 
would also check that an argument without `knownnotnull` gets no `isnull` in 
the predicate.
   
   ```suggestion
         // The optimizer drops a repeated `isnull`, as in `f(a, a)`, so check 
each argument once.
         // `semanticEquals` is false for a nondeterministic argument, so that 
guard stays native.
         ExpressionSet(guarded).toSeq
           .map(IsNull(_))
           .reduceLeftOption[Expression](Or)
           .exists(_.semanticEquals(predicate))
   ```



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