sunchao commented on code in PR #5381:
URL: https://github.com/apache/datafusion-comet/pull/5381#discussion_r3792143853


##########
spark/src/test/scala/org/apache/spark/sql/benchmark/CometExplodeBenchmark.scala:
##########
@@ -0,0 +1,135 @@
+/*
+ * 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 java.io.File
+
+/**
+ * Benchmark to measure performance of Comet's explode operator 
(`CometExplodeExec`) against
+ * Spark's `GenerateExec`, across the dimensions that drive generator cost: 
fan-out, generator
+ * variant, element type, and the number of columns replicated alongside the 
generated one. To
+ * run:
+ * {{{
+ *   SPARK_GENERATE_BENCHMARK_FILES=1 make 
benchmark-org.apache.spark.sql.benchmark.CometExplodeBenchmark
+ * }}}
+ *
+ * `runExpressionBenchmark` reports whole-query totals, so the times below 
also include the
+ * Parquet scan, the result transfer, and the per-iteration query planning. 
That fixed cost is a
+ * large share of the total at fan-out 2, where it compresses the ratio 
between the two engines,
+ * and a small one at fan-out 100. Issue #5363 tracks reporting operator cost 
against a scan
+ * baseline instead; when that lands, this paragraph should go.
+ */
+object CometExplodeBenchmark extends CometBenchmarkBase {
+
+  private val numRows = 256 * 1024
+
+  /**
+   * A SQL expression for an array column of `len` elements of `elementExpr`, 
where `elementExpr`
+   * may reference the row's `id` and the element's one-based position `x`.
+   *
+   * One in ten rows holds a null array and another one in ten holds an empty 
array, so that
+   * `explode` and `explode_outer` are a real comparison rather than the same 
query twice: the
+   * outer variants emit a null row for those 20% of rows where the plain 
variants emit nothing.
+   *
+   * The empty array is built with `slice`, not `array()`, because `array()` 
types as
+   * `array<null>` and would give that row's column a different element type.
+   */
+  private def arrayColumn(elementExpr: String, len: Int): String = {
+    val full = s"transform(sequence(1, $len), x -> $elementExpr)"
+    s"""CASE
+       |  WHEN id % 10 = 0 THEN NULL
+       |  WHEN id % 10 = 1 THEN slice($full, 1, 0)
+       |  ELSE $full
+       |END AS arr""".stripMargin
+  }
+
+  /**
+   * The temp views the benchmark reads, each with the expressions that build 
it.
+   *
+   * Each array column gets its own view rather than sharing one wide table, 
so that a case is
+   * never charged for scanning an array column it does not read.
+   */
+  private val datasets: Seq[(String, Seq[String])] = Seq(
+    "arr_len2" -> Seq(arrayColumn("id + x", 2)),
+    "arr_len10" -> Seq(arrayColumn("id + x", 10)),
+    "arr_len100" -> Seq(arrayColumn("id + x", 100)),
+    "arr_str10" -> Seq(arrayColumn("concat('str_', CAST(id + x AS STRING))", 
10)),
+    "arr_struct10" -> Seq(
+      arrayColumn("struct(id + x AS a, concat('s', CAST(x AS STRING)) AS b)", 
10)),
+    "arr_carry" -> Seq(
+      arrayColumn("id + x", 10),
+      "id AS k",
+      "CAST(id AS STRING) AS s",
+      "id * 2 AS v"))
+
+  /** Writes `selectExprs` over `numRows` rows to Parquet and registers it as 
a temp view. */
+  private def createView(dir: File, name: String, selectExprs: Seq[String]): 
Unit = {
+    val path = s"${dir.getAbsolutePath}/$name"
+    spark.range(numRows).selectExpr(selectExprs: _*).write.parquet(path)
+    spark.read.parquet(path).createOrReplaceTempView(name)
+  }
+
+  override def runCometBenchmark(mainArgs: Array[String]): Unit = {
+    withTempPath { dir =>
+      withTempTable(datasets.map(_._1): _*) {
+        datasets.foreach { case (name, selectExprs) => createView(dir, name, 
selectExprs) }
+
+        // Cardinality is input rows for every case, so the numbers are per 
scanned row rather
+        // than per generated row. Fan-out is named in the case title: the 
100-element case emits
+        // roughly 50 times as many rows as the 2-element case from the same 
256K inputs.
+        runBenchmark("Explode - fan-out") {
+          Seq(2, 10, 100).foreach { len =>
+            runExpressionBenchmark(

Review Comment:
   [P2] Isolate explode from per-generated-row columnar-to-row conversion
   
   `runExpressionBenchmark` executes `.noop()`, which writes to a 
`DataWriter[InternalRow]`. Therefore the Comet case converts every generated 
columnar output row to a Spark row, whereas Spark's `GenerateExec` already 
emits rows; the existing `CometColumnarToRowBenchmark` deliberately uses this 
same `.noop()` pattern to measure that conversion. Here fan-out 2 produces 
419,428 rows, while fan-out 100 produces 20,971,400 rows, so conversion work 
scales 50x with the dimension attributed to explode and also varies with 
strings, structs, and carried columns. This is not fixed result-transfer 
overhead, and a scan-only baseline will not remove it. Consume generated 
batches without a row boundary, or include a matched columnar-to-row baseline 
so the benchmark isolates `CometExplodeExec`.



##########
spark/src/test/scala/org/apache/spark/sql/benchmark/CometExplodeBenchmark.scala:
##########
@@ -0,0 +1,135 @@
+/*
+ * 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 java.io.File
+
+/**
+ * Benchmark to measure performance of Comet's explode operator 
(`CometExplodeExec`) against
+ * Spark's `GenerateExec`, across the dimensions that drive generator cost: 
fan-out, generator
+ * variant, element type, and the number of columns replicated alongside the 
generated one. To
+ * run:
+ * {{{
+ *   SPARK_GENERATE_BENCHMARK_FILES=1 make 
benchmark-org.apache.spark.sql.benchmark.CometExplodeBenchmark
+ * }}}
+ *
+ * `runExpressionBenchmark` reports whole-query totals, so the times below 
also include the
+ * Parquet scan, the result transfer, and the per-iteration query planning. 
That fixed cost is a
+ * large share of the total at fan-out 2, where it compresses the ratio 
between the two engines,
+ * and a small one at fan-out 100. Issue #5363 tracks reporting operator cost 
against a scan
+ * baseline instead; when that lands, this paragraph should go.
+ */
+object CometExplodeBenchmark extends CometBenchmarkBase {
+
+  private val numRows = 256 * 1024
+
+  /**
+   * A SQL expression for an array column of `len` elements of `elementExpr`, 
where `elementExpr`
+   * may reference the row's `id` and the element's one-based position `x`.
+   *
+   * One in ten rows holds a null array and another one in ten holds an empty 
array, so that
+   * `explode` and `explode_outer` are a real comparison rather than the same 
query twice: the
+   * outer variants emit a null row for those 20% of rows where the plain 
variants emit nothing.
+   *
+   * The empty array is built with `slice`, not `array()`, because `array()` 
types as
+   * `array<null>` and would give that row's column a different element type.
+   */
+  private def arrayColumn(elementExpr: String, len: Int): String = {
+    val full = s"transform(sequence(1, $len), x -> $elementExpr)"
+    s"""CASE
+       |  WHEN id % 10 = 0 THEN NULL
+       |  WHEN id % 10 = 1 THEN slice($full, 1, 0)
+       |  ELSE $full
+       |END AS arr""".stripMargin
+  }
+
+  /**
+   * The temp views the benchmark reads, each with the expressions that build 
it.
+   *
+   * Each array column gets its own view rather than sharing one wide table, 
so that a case is
+   * never charged for scanning an array column it does not read.
+   */
+  private val datasets: Seq[(String, Seq[String])] = Seq(
+    "arr_len2" -> Seq(arrayColumn("id + x", 2)),
+    "arr_len10" -> Seq(arrayColumn("id + x", 10)),
+    "arr_len100" -> Seq(arrayColumn("id + x", 100)),
+    "arr_str10" -> Seq(arrayColumn("concat('str_', CAST(id + x AS STRING))", 
10)),
+    "arr_struct10" -> Seq(
+      arrayColumn("struct(id + x AS a, concat('s', CAST(x AS STRING)) AS b)", 
10)),

Review Comment:
   [P2] Match string cardinality across the element-type datasets
   
   `arr_str10` contains 262,151 distinct `str_<id+x>` values, while this 
struct's string field contains only the 10 values `s1` through `s10`, across 
the same 2,097,140 non-null elements. The default Parquet writer enables 
dictionaries with a 1 MiB dictionary-page threshold, so the struct field stays 
dictionary-encoded while the high-cardinality standalone string field exceeds 
that threshold. Consequently the advertised string-versus-struct comparison 
also measures substantially different Parquet encoding, decoding, compression, 
and scan work. Make `struct.b` row-varying like the standalone string case, or 
explicitly match the value distribution and encoding before comparing element 
types.



##########
spark/src/test/scala/org/apache/spark/sql/benchmark/CometExplodeBenchmark.scala:
##########
@@ -0,0 +1,135 @@
+/*
+ * 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 java.io.File
+
+/**
+ * Benchmark to measure performance of Comet's explode operator 
(`CometExplodeExec`) against
+ * Spark's `GenerateExec`, across the dimensions that drive generator cost: 
fan-out, generator
+ * variant, element type, and the number of columns replicated alongside the 
generated one. To
+ * run:
+ * {{{
+ *   SPARK_GENERATE_BENCHMARK_FILES=1 make 
benchmark-org.apache.spark.sql.benchmark.CometExplodeBenchmark
+ * }}}
+ *
+ * `runExpressionBenchmark` reports whole-query totals, so the times below 
also include the
+ * Parquet scan, the result transfer, and the per-iteration query planning. 
That fixed cost is a
+ * large share of the total at fan-out 2, where it compresses the ratio 
between the two engines,
+ * and a small one at fan-out 100. Issue #5363 tracks reporting operator cost 
against a scan
+ * baseline instead; when that lands, this paragraph should go.
+ */
+object CometExplodeBenchmark extends CometBenchmarkBase {
+
+  private val numRows = 256 * 1024
+
+  /**
+   * A SQL expression for an array column of `len` elements of `elementExpr`, 
where `elementExpr`
+   * may reference the row's `id` and the element's one-based position `x`.
+   *
+   * One in ten rows holds a null array and another one in ten holds an empty 
array, so that
+   * `explode` and `explode_outer` are a real comparison rather than the same 
query twice: the
+   * outer variants emit a null row for those 20% of rows where the plain 
variants emit nothing.
+   *
+   * The empty array is built with `slice`, not `array()`, because `array()` 
types as
+   * `array<null>` and would give that row's column a different element type.
+   */
+  private def arrayColumn(elementExpr: String, len: Int): String = {
+    val full = s"transform(sequence(1, $len), x -> $elementExpr)"
+    s"""CASE
+       |  WHEN id % 10 = 0 THEN NULL
+       |  WHEN id % 10 = 1 THEN slice($full, 1, 0)
+       |  ELSE $full
+       |END AS arr""".stripMargin
+  }
+
+  /**
+   * The temp views the benchmark reads, each with the expressions that build 
it.
+   *
+   * Each array column gets its own view rather than sharing one wide table, 
so that a case is
+   * never charged for scanning an array column it does not read.
+   */
+  private val datasets: Seq[(String, Seq[String])] = Seq(
+    "arr_len2" -> Seq(arrayColumn("id + x", 2)),
+    "arr_len10" -> Seq(arrayColumn("id + x", 10)),
+    "arr_len100" -> Seq(arrayColumn("id + x", 100)),
+    "arr_str10" -> Seq(arrayColumn("concat('str_', CAST(id + x AS STRING))", 
10)),
+    "arr_struct10" -> Seq(
+      arrayColumn("struct(id + x AS a, concat('s', CAST(x AS STRING)) AS b)", 
10)),
+    "arr_carry" -> Seq(
+      arrayColumn("id + x", 10),
+      "id AS k",
+      "CAST(id AS STRING) AS s",
+      "id * 2 AS v"))
+
+  /** Writes `selectExprs` over `numRows` rows to Parquet and registers it as 
a temp view. */
+  private def createView(dir: File, name: String, selectExprs: Seq[String]): 
Unit = {
+    val path = s"${dir.getAbsolutePath}/$name"
+    spark.range(numRows).selectExpr(selectExprs: _*).write.parquet(path)
+    spark.read.parquet(path).createOrReplaceTempView(name)
+  }
+
+  override def runCometBenchmark(mainArgs: Array[String]): Unit = {
+    withTempPath { dir =>
+      withTempTable(datasets.map(_._1): _*) {
+        datasets.foreach { case (name, selectExprs) => createView(dir, name, 
selectExprs) }
+
+        // Cardinality is input rows for every case, so the numbers are per 
scanned row rather
+        // than per generated row. Fan-out is named in the case title: the 
100-element case emits
+        // roughly 50 times as many rows as the 2-element case from the same 
256K inputs.
+        runBenchmark("Explode - fan-out") {
+          Seq(2, 10, 100).foreach { len =>
+            runExpressionBenchmark(
+              s"explode array<bigint>[$len]",
+              numRows,
+              s"SELECT explode(arr) FROM arr_len$len")
+          }
+        }
+
+        runBenchmark("Explode - generator variants") {
+          Seq("explode", "posexplode", "explode_outer", 
"posexplode_outer").foreach { generator =>
+            runExpressionBenchmark(
+              s"$generator array<bigint>[10]",
+              numRows,
+              s"SELECT $generator(arr) FROM arr_len10")
+          }
+        }
+
+        runBenchmark("Explode - element type") {
+          Seq("bigint" -> "arr_len10", "string" -> "arr_str10", "struct" -> 
"arr_struct10")
+            .foreach { case (elementType, view) =>
+              runExpressionBenchmark(
+                s"explode array<$elementType>[10]",
+                numRows,
+                s"SELECT explode(arr) FROM $view")
+            }
+        }
+
+        runBenchmark("Explode - carried columns") {
+          runExpressionBenchmark("explode alone", numRows, "SELECT 
explode(arr) FROM arr_carry")
+          runExpressionBenchmark(
+            "explode plus 3 carried columns",
+            numRows,
+            "SELECT k, s, v, explode(arr) FROM arr_carry")

Review Comment:
   [P2] Keep the Parquet scan schema constant when measuring carried columns
   
   `SELECT explode(arr)` prunes `k`, `s`, and `v` from the Parquet scan, 
whereas this query must scan and decode all three additional columns before 
explode runs. Across 262,144 input rows, that adds 786,432 scalar values, 
including 262,144 distinct strings, although both cases produce the same 
2,097,140 generated rows. The measured difference therefore combines extra 
Parquet I/O and string decoding with carried-column replication, so it cannot 
isolate the stated carried-column dimension. Keep the projected scan schema 
identical between the two cases, or subtract a schema-matched scan baseline.



##########
spark/src/test/scala/org/apache/spark/sql/benchmark/CometExplodeBenchmark.scala:
##########
@@ -0,0 +1,135 @@
+/*
+ * 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 java.io.File
+
+/**
+ * Benchmark to measure performance of Comet's explode operator 
(`CometExplodeExec`) against
+ * Spark's `GenerateExec`, across the dimensions that drive generator cost: 
fan-out, generator
+ * variant, element type, and the number of columns replicated alongside the 
generated one. To
+ * run:
+ * {{{
+ *   SPARK_GENERATE_BENCHMARK_FILES=1 make 
benchmark-org.apache.spark.sql.benchmark.CometExplodeBenchmark
+ * }}}
+ *
+ * `runExpressionBenchmark` reports whole-query totals, so the times below 
also include the
+ * Parquet scan, the result transfer, and the per-iteration query planning. 
That fixed cost is a
+ * large share of the total at fan-out 2, where it compresses the ratio 
between the two engines,
+ * and a small one at fan-out 100. Issue #5363 tracks reporting operator cost 
against a scan
+ * baseline instead; when that lands, this paragraph should go.
+ */
+object CometExplodeBenchmark extends CometBenchmarkBase {
+
+  private val numRows = 256 * 1024
+
+  /**
+   * A SQL expression for an array column of `len` elements of `elementExpr`, 
where `elementExpr`
+   * may reference the row's `id` and the element's one-based position `x`.
+   *
+   * One in ten rows holds a null array and another one in ten holds an empty 
array, so that
+   * `explode` and `explode_outer` are a real comparison rather than the same 
query twice: the
+   * outer variants emit a null row for those 20% of rows where the plain 
variants emit nothing.
+   *
+   * The empty array is built with `slice`, not `array()`, because `array()` 
types as
+   * `array<null>` and would give that row's column a different element type.
+   */
+  private def arrayColumn(elementExpr: String, len: Int): String = {
+    val full = s"transform(sequence(1, $len), x -> $elementExpr)"
+    s"""CASE
+       |  WHEN id % 10 = 0 THEN NULL
+       |  WHEN id % 10 = 1 THEN slice($full, 1, 0)
+       |  ELSE $full
+       |END AS arr""".stripMargin
+  }
+
+  /**
+   * The temp views the benchmark reads, each with the expressions that build 
it.
+   *
+   * Each array column gets its own view rather than sharing one wide table, 
so that a case is
+   * never charged for scanning an array column it does not read.
+   */
+  private val datasets: Seq[(String, Seq[String])] = Seq(
+    "arr_len2" -> Seq(arrayColumn("id + x", 2)),
+    "arr_len10" -> Seq(arrayColumn("id + x", 10)),
+    "arr_len100" -> Seq(arrayColumn("id + x", 100)),
+    "arr_str10" -> Seq(arrayColumn("concat('str_', CAST(id + x AS STRING))", 
10)),
+    "arr_struct10" -> Seq(
+      arrayColumn("struct(id + x AS a, concat('s', CAST(x AS STRING)) AS b)", 
10)),
+    "arr_carry" -> Seq(
+      arrayColumn("id + x", 10),
+      "id AS k",
+      "CAST(id AS STRING) AS s",
+      "id * 2 AS v"))
+
+  /** Writes `selectExprs` over `numRows` rows to Parquet and registers it as 
a temp view. */
+  private def createView(dir: File, name: String, selectExprs: Seq[String]): 
Unit = {
+    val path = s"${dir.getAbsolutePath}/$name"
+    spark.range(numRows).selectExpr(selectExprs: _*).write.parquet(path)
+    spark.read.parquet(path).createOrReplaceTempView(name)
+  }
+
+  override def runCometBenchmark(mainArgs: Array[String]): Unit = {
+    withTempPath { dir =>
+      withTempTable(datasets.map(_._1): _*) {
+        datasets.foreach { case (name, selectExprs) => createView(dir, name, 
selectExprs) }
+
+        // Cardinality is input rows for every case, so the numbers are per 
scanned row rather
+        // than per generated row. Fan-out is named in the case title: the 
100-element case emits
+        // roughly 50 times as many rows as the 2-element case from the same 
256K inputs.
+        runBenchmark("Explode - fan-out") {
+          Seq(2, 10, 100).foreach { len =>
+            runExpressionBenchmark(
+              s"explode array<bigint>[$len]",
+              numRows,
+              s"SELECT explode(arr) FROM arr_len$len")
+          }
+        }
+
+        runBenchmark("Explode - generator variants") {
+          Seq("explode", "posexplode", "explode_outer", 
"posexplode_outer").foreach { generator =>

Review Comment:
   [P2] Prevent optimizer filtering from changing generator inputs
   
   Catalyst's `InferFiltersFromGenerate` inserts `size(arr) > 0 AND arr IS NOT 
NULL` below `explode` and `posexplode`, but explicitly does not apply to their 
outer variants. With these generated datasets, the non-outer operators 
therefore receive only 209,714 rows, while `explode_outer` and 
`posexplode_outer` receive all 262,144; none of the 52,430 null or empty arrays 
reaches the non-outer operator. The purported generator-variant comparison 
consequently mixes an extra upstream filter and different operator input 
cardinalities, and its non-outer rate is normalized using rows the generator 
never processes. Exclude `InferFiltersFromGenerate` symmetrically for Spark and 
Comet, preserving the helper's existing `ConstantFolding` exclusion, or 
otherwise give both variants matched operator inputs.



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