This is an automated email from the ASF dual-hosted git repository.

MaxGekk pushed a commit to branch branch-4.x
in repository https://gitbox.apache.org/repos/asf/spark.git


The following commit(s) were added to refs/heads/branch-4.x by this push:
     new 3d1b2d3c452e [SPARK-56893][SQL][TEST] Add 
ParquetDictionaryDecodeBenchmark for dictionary ID decode throughput
3d1b2d3c452e is described below

commit 3d1b2d3c452e8838c7d4b3ead2478b05ccc38731
Author: Ismaël Mejía <[email protected]>
AuthorDate: Mon Jun 22 22:01:29 2026 +0200

    [SPARK-56893][SQL][TEST] Add ParquetDictionaryDecodeBenchmark for 
dictionary ID decode throughput
    
    ### What changes were proposed in this pull request?
    
    Add a micro-benchmark for `ParquetVectorUpdater.decodeDictionaryIds` -- the 
second pass of dictionary-encoded Parquet reads that translates dictionary IDs 
into decoded values.
    
    Coverage:
    - **Core primitive Updaters:** Integer, Long, Float, Double.
    - **Type-converting Updaters:** IntegerToLong, FloatToDouble.
    
    Each group is tested with three null fractions (0%, 10%, 50%) to exercise 
the no-null fast path and the per-element null-check path.
    
    The benchmark includes a global pre-warm phase that interleaves both 
`hasNull()` branches for every updater class to ensure C2 compiles with 
balanced profiles, avoiding uncommon-trap demotion bias.
    
    ### Why are the changes needed?
    
    This establishes a committed baseline for #55920 (SPARK-56893), which 
optimizes the dictionary decode path with a `hasNull()` fast path and per-class 
updater overrides. Landing the benchmark first allows the optimization PR to 
regenerate the results, so the `.txt` diff directly shows the throughput gain 
-- matching the pattern used in #55922 and #55924.
    
    ### Does this PR introduce _any_ user-facing change?
    
    No.
    
    ### How was this patch tested?
    
    The benchmark compiles and runs successfully:
    ```
    build/sbt "sql/Test/runMain 
org.apache.spark.sql.execution.datasources.parquet.ParquetDictionaryDecodeBenchmark"
    ```
    
    Benchmark results will be generated via the GitHub Actions benchmark 
workflow.
    
    ### Was this patch authored or co-authored using generative AI tooling?
    
    Generated-by: GitHub Copilot (claude-opus-4.6)
    
    Closes #56670 from iemejia/SPARK-56893-parquet-dict-decode-benchmark.
    
    Authored-by: Ismaël Mejía <[email protected]>
    Signed-off-by: Max Gekk <[email protected]>
    (cherry picked from commit ba8e7fe6f0443da07dba88118de448ef982962b0)
    Signed-off-by: Max Gekk <[email protected]>
---
 ...quetDictionaryDecodeBenchmark-jdk21-results.txt |  48 +++++
 ...quetDictionaryDecodeBenchmark-jdk25-results.txt |  48 +++++
 .../ParquetDictionaryDecodeBenchmark-results.txt   |  48 +++++
 .../parquet/ParquetDictionaryDecodeBenchmark.scala | 229 +++++++++++++++++++++
 4 files changed, 373 insertions(+)

diff --git 
a/sql/core/benchmarks/ParquetDictionaryDecodeBenchmark-jdk21-results.txt 
b/sql/core/benchmarks/ParquetDictionaryDecodeBenchmark-jdk21-results.txt
new file mode 100644
index 000000000000..041372e7f0af
--- /dev/null
+++ b/sql/core/benchmarks/ParquetDictionaryDecodeBenchmark-jdk21-results.txt
@@ -0,0 +1,48 @@
+================================================================================================
+Dictionary Decode (no nulls)
+================================================================================================
+
+OpenJDK 64-Bit Server VM 21.0.11+10-LTS on Linux 6.17.0-1018-azure
+AMD EPYC 9V74 80-Core Processor
+Dictionary Decode (no nulls):             Best Time(ms)   Avg Time(ms)   
Stdev(ms)    Rate(M/s)   Per Row(ns)   Relative
+------------------------------------------------------------------------------------------------------------------------
+IntegerUpdater                                        6              6         
  0        177.6           5.6       1.0X
+LongUpdater                                           6              6         
  0        176.4           5.7       1.0X
+FloatUpdater                                          6              6         
  0        178.3           5.6       1.0X
+DoubleUpdater                                         6              6         
  0        177.2           5.6       1.0X
+IntegerToLongUpdater (INT32 -> Long)                  6              6         
  0        175.8           5.7       1.0X
+FloatToDoubleUpdater (FLOAT -> Double)                6              6         
  0        172.2           5.8       1.0X
+
+
+================================================================================================
+Dictionary Decode (10% nulls)
+================================================================================================
+
+OpenJDK 64-Bit Server VM 21.0.11+10-LTS on Linux 6.17.0-1018-azure
+AMD EPYC 9V74 80-Core Processor
+Dictionary Decode (10% nulls):            Best Time(ms)   Avg Time(ms)   
Stdev(ms)    Rate(M/s)   Per Row(ns)   Relative
+------------------------------------------------------------------------------------------------------------------------
+IntegerUpdater                                        7              7         
  0        159.9           6.3       1.0X
+LongUpdater                                           7              7         
  0        158.4           6.3       1.0X
+FloatUpdater                                          7              7         
  0        159.9           6.3       1.0X
+DoubleUpdater                                         7              7         
  0        159.8           6.3       1.0X
+IntegerToLongUpdater (INT32 -> Long)                  7              7         
  0        160.4           6.2       1.0X
+FloatToDoubleUpdater (FLOAT -> Double)                7              7         
  0        157.0           6.4       1.0X
+
+
+================================================================================================
+Dictionary Decode (50% nulls)
+================================================================================================
+
+OpenJDK 64-Bit Server VM 21.0.11+10-LTS on Linux 6.17.0-1018-azure
+AMD EPYC 9V74 80-Core Processor
+Dictionary Decode (50% nulls):            Best Time(ms)   Avg Time(ms)   
Stdev(ms)    Rate(M/s)   Per Row(ns)   Relative
+------------------------------------------------------------------------------------------------------------------------
+IntegerUpdater                                        9              9         
  0        113.2           8.8       1.0X
+LongUpdater                                           9              9         
  0        117.3           8.5       1.0X
+FloatUpdater                                          9              9         
  0        117.7           8.5       1.0X
+DoubleUpdater                                         9              9         
  0        117.4           8.5       1.0X
+IntegerToLongUpdater (INT32 -> Long)                  9              9         
  0        113.5           8.8       1.0X
+FloatToDoubleUpdater (FLOAT -> Double)                9              9         
  0        117.6           8.5       1.0X
+
+
diff --git 
a/sql/core/benchmarks/ParquetDictionaryDecodeBenchmark-jdk25-results.txt 
b/sql/core/benchmarks/ParquetDictionaryDecodeBenchmark-jdk25-results.txt
new file mode 100644
index 000000000000..538cc8c3445b
--- /dev/null
+++ b/sql/core/benchmarks/ParquetDictionaryDecodeBenchmark-jdk25-results.txt
@@ -0,0 +1,48 @@
+================================================================================================
+Dictionary Decode (no nulls)
+================================================================================================
+
+OpenJDK 64-Bit Server VM 25.0.3+9-LTS on Linux 6.17.0-1018-azure
+AMD EPYC 9V74 80-Core Processor
+Dictionary Decode (no nulls):             Best Time(ms)   Avg Time(ms)   
Stdev(ms)    Rate(M/s)   Per Row(ns)   Relative
+------------------------------------------------------------------------------------------------------------------------
+IntegerUpdater                                        6              6         
  0        179.1           5.6       1.0X
+LongUpdater                                           6              6         
  0        178.6           5.6       1.0X
+FloatUpdater                                          6              6         
  0        178.1           5.6       1.0X
+DoubleUpdater                                         6              6         
  0        177.5           5.6       1.0X
+IntegerToLongUpdater (INT32 -> Long)                  6              6         
  0        175.1           5.7       1.0X
+FloatToDoubleUpdater (FLOAT -> Double)                6              6         
  0        178.8           5.6       1.0X
+
+
+================================================================================================
+Dictionary Decode (10% nulls)
+================================================================================================
+
+OpenJDK 64-Bit Server VM 25.0.3+9-LTS on Linux 6.17.0-1018-azure
+AMD EPYC 9V74 80-Core Processor
+Dictionary Decode (10% nulls):            Best Time(ms)   Avg Time(ms)   
Stdev(ms)    Rate(M/s)   Per Row(ns)   Relative
+------------------------------------------------------------------------------------------------------------------------
+IntegerUpdater                                        7              7         
  0        159.0           6.3       1.0X
+LongUpdater                                           7              7         
  0        159.3           6.3       1.0X
+FloatUpdater                                          7              7         
  0        158.6           6.3       1.0X
+DoubleUpdater                                         7              7         
  0        158.2           6.3       1.0X
+IntegerToLongUpdater (INT32 -> Long)                  7              7         
  0        158.6           6.3       1.0X
+FloatToDoubleUpdater (FLOAT -> Double)                7              7         
  0        159.2           6.3       1.0X
+
+
+================================================================================================
+Dictionary Decode (50% nulls)
+================================================================================================
+
+OpenJDK 64-Bit Server VM 25.0.3+9-LTS on Linux 6.17.0-1018-azure
+AMD EPYC 9V74 80-Core Processor
+Dictionary Decode (50% nulls):            Best Time(ms)   Avg Time(ms)   
Stdev(ms)    Rate(M/s)   Per Row(ns)   Relative
+------------------------------------------------------------------------------------------------------------------------
+IntegerUpdater                                        9              9         
  0        116.4           8.6       1.0X
+LongUpdater                                           9              9         
  0        111.4           9.0       1.0X
+FloatUpdater                                          9              9         
  0        116.4           8.6       1.0X
+DoubleUpdater                                         9              9         
  0        116.6           8.6       1.0X
+IntegerToLongUpdater (INT32 -> Long)                  9              9         
  0        114.0           8.8       1.0X
+FloatToDoubleUpdater (FLOAT -> Double)                9              9         
  0        116.2           8.6       1.0X
+
+
diff --git a/sql/core/benchmarks/ParquetDictionaryDecodeBenchmark-results.txt 
b/sql/core/benchmarks/ParquetDictionaryDecodeBenchmark-results.txt
new file mode 100644
index 000000000000..c43127858a13
--- /dev/null
+++ b/sql/core/benchmarks/ParquetDictionaryDecodeBenchmark-results.txt
@@ -0,0 +1,48 @@
+================================================================================================
+Dictionary Decode (no nulls)
+================================================================================================
+
+OpenJDK 64-Bit Server VM 17.0.19+10-LTS on Linux 6.17.0-1018-azure
+AMD EPYC 9V74 80-Core Processor
+Dictionary Decode (no nulls):             Best Time(ms)   Avg Time(ms)   
Stdev(ms)    Rate(M/s)   Per Row(ns)   Relative
+------------------------------------------------------------------------------------------------------------------------
+IntegerUpdater                                        6              6         
  0        186.7           5.4       1.0X
+LongUpdater                                           6              6         
  0        185.1           5.4       1.0X
+FloatUpdater                                          6              6         
  0        188.6           5.3       1.0X
+DoubleUpdater                                         6              6         
  0        187.6           5.3       1.0X
+IntegerToLongUpdater (INT32 -> Long)                  6              6         
  0        184.4           5.4       1.0X
+FloatToDoubleUpdater (FLOAT -> Double)                6              6         
  0        187.3           5.3       1.0X
+
+
+================================================================================================
+Dictionary Decode (10% nulls)
+================================================================================================
+
+OpenJDK 64-Bit Server VM 17.0.19+10-LTS on Linux 6.17.0-1018-azure
+AMD EPYC 9V74 80-Core Processor
+Dictionary Decode (10% nulls):            Best Time(ms)   Avg Time(ms)   
Stdev(ms)    Rate(M/s)   Per Row(ns)   Relative
+------------------------------------------------------------------------------------------------------------------------
+IntegerUpdater                                        6              6         
  0        164.8           6.1       1.0X
+LongUpdater                                           6              6         
  0        165.7           6.0       1.0X
+FloatUpdater                                          6              6         
  0        166.1           6.0       1.0X
+DoubleUpdater                                         6              6         
  0        165.7           6.0       1.0X
+IntegerToLongUpdater (INT32 -> Long)                  6              6         
  0        165.7           6.0       1.0X
+FloatToDoubleUpdater (FLOAT -> Double)                6              6         
  0        165.7           6.0       1.0X
+
+
+================================================================================================
+Dictionary Decode (50% nulls)
+================================================================================================
+
+OpenJDK 64-Bit Server VM 17.0.19+10-LTS on Linux 6.17.0-1018-azure
+AMD EPYC 9V74 80-Core Processor
+Dictionary Decode (50% nulls):            Best Time(ms)   Avg Time(ms)   
Stdev(ms)    Rate(M/s)   Per Row(ns)   Relative
+------------------------------------------------------------------------------------------------------------------------
+IntegerUpdater                                        9              9         
  0        117.7           8.5       1.0X
+LongUpdater                                           9              9         
  0        116.2           8.6       1.0X
+FloatUpdater                                          9              9         
  0        117.7           8.5       1.0X
+DoubleUpdater                                         9              9         
  0        117.6           8.5       1.0X
+IntegerToLongUpdater (INT32 -> Long)                  9              9         
  0        118.6           8.4       1.0X
+FloatToDoubleUpdater (FLOAT -> Double)                9              9         
  0        117.4           8.5       1.0X
+
+
diff --git 
a/sql/core/src/test/scala/org/apache/spark/sql/execution/datasources/parquet/ParquetDictionaryDecodeBenchmark.scala
 
b/sql/core/src/test/scala/org/apache/spark/sql/execution/datasources/parquet/ParquetDictionaryDecodeBenchmark.scala
new file mode 100644
index 000000000000..e7e2e6881fb1
--- /dev/null
+++ 
b/sql/core/src/test/scala/org/apache/spark/sql/execution/datasources/parquet/ParquetDictionaryDecodeBenchmark.scala
@@ -0,0 +1,229 @@
+/*
+ * 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.execution.datasources.parquet
+
+import java.time.ZoneOffset
+
+import org.apache.parquet.column.{ColumnDescriptor, Encoding}
+import org.apache.parquet.schema.{LogicalTypeAnnotation, Types}
+import org.apache.parquet.schema.PrimitiveType.PrimitiveTypeName
+import org.apache.parquet.schema.Type.Repetition
+
+import org.apache.spark.benchmark.{Benchmark, BenchmarkBase}
+import org.apache.spark.sql.execution.vectorized.{OnHeapColumnVector, 
WritableColumnVector}
+import org.apache.spark.sql.types._
+
+/**
+ * Benchmark for `ParquetVectorUpdater.decodeDictionaryIds` -- the second pass 
of
+ * dictionary-encoded Parquet reads. After 
`VectorizedRleValuesReader.readIntegers`
+ * populates dictionary IDs and null markers, `decodeDictionaryIds` translates 
the IDs
+ * into decoded values.
+ *
+ * Coverage:
+ *   A. Core primitive Updaters: Integer, Long, Float, Double.
+ *   B. Type-converting Updaters: IntegerToLong, FloatToDouble.
+ *
+ * Each group is tested with three null fractions (0%, 10%, 50%) to exercise 
the
+ * no-null fast path and the per-element null-check path.
+ *
+ * The dictionary is an anonymous `org.apache.parquet.column.Dictionary` 
backed by
+ * pre-populated arrays (100 entries), matching the production decode-to-xxx 
methods.
+ * Dictionary IDs are uniform-random in [0, 100).
+ *
+ * JIT note: `decodeDictionaryIds` has two branches (no-null vs has-null). 
Running one
+ * branch extensively can bias the JIT against the other via uncommon-trap 
demotion.
+ * A global pre-warm phase interleaves both branches for every updater class 
before any
+ * measurement to ensure C2 compiles with balanced profiles.
+ *
+ * To run this benchmark:
+ * {{{
+ *   1. build/sbt "sql/Test/runMain <this class>"
+ *   2. generate result:
+ *      SPARK_GENERATE_BENCHMARK_FILES=1 build/sbt "sql/Test/runMain <this 
class>"
+ *      Results in "benchmarks/ParquetDictionaryDecodeBenchmark-results.txt".
+ * }}}
+ */
+object ParquetDictionaryDecodeBenchmark extends BenchmarkBase {
+
+  private val NUM_ROWS = 1024 * 1024
+  private val NUM_ITERS = 5
+  private val DICT_SIZE = 100
+
+  // --------------- Helpers ---------------
+
+  private def descriptor(
+      name: PrimitiveTypeName,
+      logical: LogicalTypeAnnotation = null): ColumnDescriptor = {
+    var builder = Types.primitive(name, Repetition.OPTIONAL)
+    if (logical != null) builder = builder.as(logical)
+    new ColumnDescriptor(Array("col"), builder.named("col"), 0, 1)
+  }
+
+  private def factory(desc: ColumnDescriptor): ParquetVectorUpdaterFactory =
+    ParquetTestAccess.newFactory(
+      desc.getPrimitiveType.getLogicalTypeAnnotation,
+      ZoneOffset.UTC, "CORRECTED", "UTC", "CORRECTED", "UTC")
+
+  /**
+   * Creates a parquet-mr Dictionary backed by pre-populated arrays.
+   * Supports decodeToInt, decodeToLong, decodeToFloat, decodeToDouble.
+   */
+  private def createDictionary(size: Int): 
org.apache.parquet.column.Dictionary = {
+    val intVals = Array.tabulate(size)(i => i * 7)
+    val longVals = Array.tabulate(size)(i => i.toLong * 13)
+    val floatVals = Array.tabulate(size)(i => i * 0.1f)
+    val doubleVals = Array.tabulate(size)(i => i * 0.01)
+
+    new org.apache.parquet.column.Dictionary(Encoding.PLAIN) {
+      override def getMaxId: Int = size - 1
+      override def decodeToInt(id: Int): Int = intVals(id)
+      override def decodeToLong(id: Int): Long = longVals(id)
+      override def decodeToFloat(id: Int): Float = floatVals(id)
+      override def decodeToDouble(id: Int): Double = doubleVals(id)
+    }
+  }
+
+  /** Populates a column vector with random dictionary IDs in [0, dictSize). */
+  private def populateDictIds(
+      dictIds: WritableColumnVector, count: Int, dictSize: Int): Unit = {
+    val rng = new java.util.Random(42)
+    var i = 0
+    while (i < count) {
+      dictIds.putInt(i, rng.nextInt(dictSize))
+      i += 1
+    }
+  }
+
+  /** Sets null markers on a column vector using the given null fraction. */
+  private def setNulls(
+      values: WritableColumnVector, count: Int, nullFraction: Double): Unit = {
+    val rng = new java.util.Random(123)
+    var i = 0
+    while (i < count) {
+      if (rng.nextDouble() < nullFraction) values.putNull(i)
+      i += 1
+    }
+  }
+
+  /** Updater configurations: (sparkType, descriptor). */
+  private val updaterConfigs: Seq[(DataType, ColumnDescriptor)] = Seq(
+    (IntegerType, descriptor(PrimitiveTypeName.INT32)),
+    (LongType, descriptor(PrimitiveTypeName.INT64)),
+    (FloatType, descriptor(PrimitiveTypeName.FLOAT)),
+    (DoubleType, descriptor(PrimitiveTypeName.DOUBLE)),
+    (LongType, descriptor(PrimitiveTypeName.INT32)),     // IntegerToLong
+    (DoubleType, descriptor(PrimitiveTypeName.FLOAT))    // FloatToDouble
+  )
+
+  /**
+   * Pre-warms all updater classes by interleaving no-null and has-null calls.
+   * This trains C2 to compile both `hasNull()` branches as hot paths, avoiding
+   * the uncommon-trap demotion that occurs when one branch dominates 
profiling.
+   */
+  private def globalPreWarm(dict: org.apache.parquet.column.Dictionary): Unit 
= {
+    val warmIters = 50
+    for ((sparkType, desc) <- updaterConfigs) {
+      val updater = factory(desc).getUpdater(desc, sparkType)
+
+      val noNullVec = new OnHeapColumnVector(NUM_ROWS, sparkType)
+      val nullVec = new OnHeapColumnVector(NUM_ROWS, sparkType)
+      val dictIds = new OnHeapColumnVector(NUM_ROWS, IntegerType)
+      populateDictIds(dictIds, NUM_ROWS, DICT_SIZE)
+      setNulls(nullVec, NUM_ROWS, 0.5)
+
+      var iter = 0
+      while (iter < warmIters) {
+        updater.decodeDictionaryIds(NUM_ROWS, 0, noNullVec, dictIds, dict)
+        updater.decodeDictionaryIds(NUM_ROWS, 0, nullVec, dictIds, dict)
+        iter += 1
+      }
+    }
+  }
+
+  // --------------- Per-case runner ---------------
+
+  private val updaterLabels: Seq[String] = Seq(
+    "IntegerUpdater",
+    "LongUpdater",
+    "FloatUpdater",
+    "DoubleUpdater",
+    "IntegerToLongUpdater (INT32 -> Long)",
+    "FloatToDoubleUpdater (FLOAT -> Double)"
+  )
+
+  /**
+   * Registers a benchmark case that decodes `NUM_ROWS` dictionary IDs via
+   * `updater.decodeDictionaryIds`. The values vector has null markers pre-set
+   * (for the given null fraction) and is NOT reset between iterations -- the
+   * decoder reads nulls and overwrites non-null slots, so the null state is
+   * stable across iterations.
+   */
+  private def addDictDecodeCase(
+      benchmark: Benchmark,
+      label: String,
+      sparkType: DataType,
+      desc: ColumnDescriptor,
+      dict: org.apache.parquet.column.Dictionary,
+      nullFraction: Double): Unit = {
+    val updater = factory(desc).getUpdater(desc, sparkType)
+    val values = new OnHeapColumnVector(NUM_ROWS, sparkType)
+    val dictIds = new OnHeapColumnVector(NUM_ROWS, IntegerType)
+
+    populateDictIds(dictIds, NUM_ROWS, DICT_SIZE)
+    if (nullFraction > 0.0) setNulls(values, NUM_ROWS, nullFraction)
+
+    // Per-case pre-warm (supplements globalPreWarm)
+    updater.decodeDictionaryIds(NUM_ROWS, 0, values, dictIds, dict)
+
+    benchmark.addCase(label) { _ =>
+      updater.decodeDictionaryIds(NUM_ROWS, 0, values, dictIds, dict)
+    }
+  }
+
+  // --------------- Benchmark groups ---------------
+
+  private def runDictionaryDecodeBenchmark(
+      nullFraction: Double,
+      dict: org.apache.parquet.column.Dictionary): Unit = {
+    val label = if (nullFraction == 0.0) "no nulls"
+                else s"${(nullFraction * 100).toInt}% nulls"
+    val benchmark = new Benchmark(
+      s"Dictionary Decode ($label)", NUM_ROWS.toLong, NUM_ITERS, output = 
output)
+
+    updaterConfigs.zip(updaterLabels).foreach { case ((sparkType, desc), name) 
=>
+      addDictDecodeCase(benchmark, name, sparkType, desc, dict, nullFraction)
+    }
+
+    benchmark.run()
+  }
+
+  override def runBenchmarkSuite(mainArgs: Array[String]): Unit = {
+    val dict = createDictionary(DICT_SIZE)
+    globalPreWarm(dict)
+
+    runBenchmark("Dictionary Decode (no nulls)") {
+      runDictionaryDecodeBenchmark(0.0, dict)
+    }
+    runBenchmark("Dictionary Decode (10% nulls)") {
+      runDictionaryDecodeBenchmark(0.1, dict)
+    }
+    runBenchmark("Dictionary Decode (50% nulls)") {
+      runDictionaryDecodeBenchmark(0.5, dict)
+    }
+  }
+}


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to