andygrove commented on code in PR #4744:
URL: https://github.com/apache/datafusion-comet/pull/4744#discussion_r3760640865


##########
spark/src/main/scala/org/apache/comet/serde/arrays.scala:
##########
@@ -713,9 +713,32 @@ object CometFlatten extends CometExpressionSerde[Flatten] 
with ArraysBase {
   }
 }
 
-object CometArrayFilter extends CometExpressionSerde[ArrayFilter] {
+object CometArrayFilter extends 
CometHighOrderFunction[ArrayFilter]("array_filter") {
 
-  override def getSupportLevel(expr: ArrayFilter): SupportLevel = Compatible()
+  private val UNARY_FUNCTION_EXPECTED =
+    "The array_filter function in DataFusion is limited to one lambda 
parameter"
+
+  override def getUnsupportedReasons(): Seq[String] = 
Seq(UNARY_FUNCTION_EXPECTED)

Review Comment:
   This replaces the parent's reasons rather than adding to them, so `lambda 
functions must be LambdaFunction` and `lambda arguments must be 
NamedLambdaVariables` from `CometHighOrderFunction.getUnsupportedReasons()` 
disappear from the generated compatibility docs for `filter`. Could this be 
`super.getUnsupportedReasons() ++ Seq(UNARY_FUNCTION_EXPECTED)`?



##########
spark/src/main/scala/org/apache/comet/serde/CometHighOrderFunction.scala:
##########
@@ -0,0 +1,163 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.comet.serde
+
+import scala.jdk.CollectionConverters._
+
+import org.apache.spark.sql.catalyst.expressions.{Attribute, 
HigherOrderFunction, LambdaFunction => SparkLambdaFunction, NamedLambdaVariable 
=> SparkNamedLambdaVariable}
+
+import org.apache.comet.CometConf
+import org.apache.comet.CometSparkSessionExtensions.withFallbackReason
+import org.apache.comet.serde.CometHighOrderFunction.namedLambdaVariable2Proto
+import org.apache.comet.serde.ExprOuterClass.{HigherOrderFunc, LambdaFunction, 
NamedLambdaVariable}
+import org.apache.comet.serde.QueryPlanSerde.{exprToProtoInternal, 
serializeDataType}
+
+/**
+ * Serializer that converts Spark higher-order functions (e.g. `filter`, 
`transform`, `exists`)
+ * into Comet's protobuf representation.
+ *
+ * Depending on the available configuration and on whether the expression 
satisfies the native
+ * constraints, [[convert]] produces one of two representations:
+ *   - a native higher-order function proto (executed by the DataFusion 
engine), used when
+ *     `COMET_EXEC_HIGHER_ORDER_FUNCTION_NATIVE_ENABLED` is set and the 
expression is natively
+ *     supported (see [[nativeUnsupportedReason]] / [[getSupportLevel]]); or
+ *   - a JVM codegen dispatch (Scala UDF fallback via 
`CometScalaUDF.emitJvmCodegenDispatch`),
+ *     used when the native path is unavailable but 
`COMET_SCALA_UDF_CODEGEN_ENABLED` is enabled.
+ */
+case class CometHighOrderFunction[T <: HigherOrderFunction](name: String)
+    extends CometExpressionSerde[T] {
+
+  private val UNSUPPORTED_LAMBDA_TYPE = "lambda functions must be 
LambdaFunction"
+  private val UNSUPPORTED_LAMBDA_PARAM_TYPE = "lambda arguments must be 
NamedLambdaVariables"
+
+  override def getUnsupportedReasons(): Seq[String] =
+    Seq(UNSUPPORTED_LAMBDA_TYPE, UNSUPPORTED_LAMBDA_PARAM_TYPE)
+
+  private def nativeUnsupportedReason(expr: T): Option[String] = {

Review Comment:
   I hit a case where the native path is taken but the lambda variable never 
reaches the evaluated body, so the predicate is false for every element. This 
is on fully default configuration, and it returns wrong results rather than 
erroring.
   
   ```sql
   CREATE TABLE t(sarr array<string>, a array<int>, b array<int>) USING parquet;
   INSERT INTO t VALUES (array('abc','xyz','a1'), array(1,2,3), 
array(10,20,30));
   
   SELECT filter(sarr, x -> x rlike '^a') FROM t;
   -- Spark: [abc, a1]   Comet: []
   
   SELECT filter(a, x -> exists(b, y -> y > x)) FROM t;
   -- Spark: [1, 2, 3]   Comet: []
   
   SELECT filter(a, x -> array_max(transform(b, y -> y + x)) > 31) FROM t;
   -- Spark: [2, 3]      Comet: []
   ```
   
   All three plan to `CometProject` and return empty arrays. Setting 
`spark.comet.exec.higherOrderFunction.native.enabled=false` makes all three 
correct, which points at the new native path.
   
   My reading of the mechanism: `highOrderFunction2Proto` calls 
`exprToProtoInternal` on the lambda body, and when the body contains `rlike`, a 
non-native nested higher-order function like `exists` or `transform`, or 
anything else that lands in `CometScalaUDF.emitJvmCodegenDispatch`, that call 
succeeds and emits a `JvmScalarUdf` proto. So `hofProto.isDefined` is true and 
we commit to native. But `emitJvmCodegenDispatch` binds against 
`AttributeReference`s only, and a `NamedLambdaVariable` is not one, so the 
per-element value never reaches the compiled kernel.
   
   Could `nativeUnsupportedReason` walk the lambda body and decline the native 
path when it finds a subexpression that will route through the codegen 
dispatcher, including nested `HigherOrderFunction`s? Then `convert` degrades to 
codegen the way the config doc describes.
   
   Worth SQL tests for these shapes as well. One thing to watch: `filter(a, x 
-> exists(b, y -> y > 15))` passes, because nothing crosses the lambda boundary 
there. The capture has to be meaningful for the test to catch this.



##########
spark/src/test/scala/org/apache/spark/sql/benchmark/CometArrayFilterBenchmark.scala:
##########
@@ -0,0 +1,87 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.spark.sql.benchmark
+
+import org.apache.spark.benchmark.Benchmark
+
+import org.apache.comet.CometConf
+
+// spotless:off
+/**
+ * Benchmark to measure performance of Comet array expressions. To run this 
benchmark:
+ * {{{
+ *   SPARK_GENERATE_BENCHMARK_FILES=1 make 
benchmark-org.apache.spark.sql.benchmark.CometArrayFilterBenchmark
+ * }}}
+ * Results will be written to 
"spark/benchmarks/CometArrayFilterBenchmark-**results.txt".
+ */
+// spotless:on
+object CometArrayFilterBenchmark extends CometBenchmarkBase {
+
+  def runExprBenchmark(config: ArrayFilterExprConfig, values: Int, arraySize: 
Int): Unit = {
+    val benchmark =
+      new Benchmark(s"${config.name} (size $arraySize)", values, output = 
output)
+    withTempPath { dir =>
+      withTempTable("parquetV1Table") {
+        prepareTable(
+          dir,
+          spark.sql(
+            s"SELECT sequence(0, cast(rand(42) * $arraySize as int)) AS arr " +
+              s"FROM range($values)"))
+
+        benchmark.addCase(s"Spark ${config.name}") { _ =>
+          withSQLConf(CometConf.COMET_ENABLED.key -> "false") {
+            spark.sql(config.query).noop()
+          }
+        }
+
+        benchmark.addCase(s"Comet (Native) ${config.name}") { _ =>
+          withSQLConf(
+            CometConf.COMET_ENABLED.key -> "true",
+            CometConf.COMET_EXEC_ENABLED.key -> "true",
+            CometConf.COMET_SCALA_UDF_CODEGEN_ENABLED.key -> "false") {
+            spark.sql(config.query).noop()
+          }
+        }
+
+        benchmark.addCase(s"Comet (Codegen) ${config.name}") { _ =>
+          withSQLConf(
+            CometConf.COMET_ENABLED.key -> "true",
+            CometConf.COMET_EXEC_ENABLED.key -> "true",
+            CometConf.COMET_SCALA_UDF_CODEGEN_ENABLED.key -> "true") {
+            spark.sql(config.query).noop()
+          }
+        }
+
+        benchmark.run()
+      }
+    }
+  }
+
+  def runCometBenchmark(args: Array[String]): Unit = {
+    val values = 4 * 1024 * 1024
+
+    val config =
+      ArrayFilterExprConfig("array_filter", "SELECT filter(arr, x -> x > 2) 
FROM parquetV1Table")
+
+    runExprBenchmark(config, values, 100)

Review Comment:
   The benchmark covers one shape only: `x -> x > 2` over int arrays of size 
100. Since the stated rationale is avoiding the per-batch JNI call, it would be 
more informative to also cover a predicate that captures an outer column and a 
string-element case, so we can see whether the win holds across the shapes 
people actually write.



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