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

He-Pin pushed a commit to branch perf/optimize-hash-code-arrays
in repository https://gitbox.apache.org/repos/asf/pekko.git

commit f4136f791137bf16d5af9546cd90c7a5b71ac8e6
Author: 虎鸣 <[email protected]>
AuthorDate: Fri Jul 10 20:48:44 2026 +0800

    perf: optimize JVM array hashing
    
    Motivation:
    HashCode.hash iterates JVM arrays through java.lang.reflect.Array, adding 
reflective per-element access and boxing in a shared utility.
    
    Modification:
    Dispatch all nine JVM array kinds to typed while loops. Add a directional 
test that uses the original reflective algorithm as the parity oracle across 
primitive, reference, empty, null-element, and nested arrays. Add a JMH 
benchmark for primitive and reference arrays.
    
    Result:
    Hash values remain compatible while JDK 17 JMH measurements improve by 
roughly 39x to 135x for the measured 16- and 256-element arrays.
    
    Tests:
    - JDK 17 Scala 2.13 HashCodeSpec: 3 tests passed
    - JDK 17 Scala 3.3.8 HashCodeSpec: 3 tests passed
    - JDK 17 JMH: Int[16] 824.277 to 6.106 ns/op; Int[256] 12749.433 to 183.033 
ns/op; AnyRef[16] 635.492 to 13.945 ns/op; AnyRef[256] 9491.604 to 240.387 ns/op
    - sbt +headerCheckAll and checkCodeStyle passed
    - scalafmt --list --mode diff-ref=origin/main passed
    - git diff --check passed
    - qodercli formal stdout review found no must-fix findings
    
    References:
    Refs #3300
---
 .../scala/org/apache/pekko/util/HashCodeSpec.scala | 81 ++++++++++++++++++++++
 .../scala/org/apache/pekko/util/HashCode.scala     | 65 ++++++++++++++++-
 .../apache/pekko/util/HashCodeArrayBenchmark.scala | 46 ++++++++++++
 3 files changed, 190 insertions(+), 2 deletions(-)

diff --git 
a/actor-tests/src/test/scala/org/apache/pekko/util/HashCodeSpec.scala 
b/actor-tests/src/test/scala/org/apache/pekko/util/HashCodeSpec.scala
new file mode 100644
index 0000000000..e46af45329
--- /dev/null
+++ b/actor-tests/src/test/scala/org/apache/pekko/util/HashCodeSpec.scala
@@ -0,0 +1,81 @@
+/*
+ * 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.pekko.util
+
+import java.lang.reflect.{ Array => JArray }
+
+import org.scalatest.matchers.should.Matchers
+import org.scalatest.wordspec.AnyWordSpec
+
+class HashCodeSpec extends AnyWordSpec with Matchers {
+
+  private def fold(seed: Int, values: Seq[Any]): Int =
+    values.foldLeft(seed) { (result, value) =>
+      HashCode.hash(result, value)
+    }
+
+  private def legacyHash(seed: Int, value: Any): Int = value match {
+    case array: AnyRef if array.getClass.isArray =>
+      var result = seed
+      var index = 0
+      while (index < JArray.getLength(array)) {
+        result = legacyHash(result, JArray.get(array, index))
+        index += 1
+      }
+      result
+    case _ => HashCode.hash(seed, value)
+  }
+
+  "HashCode.hash" must {
+    "preserve hashing semantics for every JVM array kind" in {
+      val seed = HashCode.SEED
+      val cases = Seq[(AnyRef, Seq[Any])](
+        Array(true, false, true) -> Seq(true, false, true),
+        Array('a', 'b') -> Seq('a', 'b'),
+        Array[Short](1, -2) -> Seq[Short](1, -2),
+        Array(1, -2, 3) -> Seq(1, -2, 3),
+        Array(1L, Long.MaxValue) -> Seq(1L, Long.MaxValue),
+        Array(1.25f, Float.NaN) -> Seq(1.25f, Float.NaN),
+        Array(1.25d, Double.NaN) -> Seq(1.25d, Double.NaN),
+        Array[Byte](1, -2) -> Seq[Byte](1, -2),
+        Array[AnyRef]("a", Integer.valueOf(3)) -> Seq("a", Integer.valueOf(3)),
+        Array.empty[Int] -> Seq.empty,
+        Array.empty[AnyRef] -> Seq.empty)
+
+      cases.foreach { case (array, elements) =>
+        val legacyResult = legacyHash(seed, array)
+        legacyResult should ===(fold(seed, elements))
+        HashCode.hash(seed, array) should ===(legacyResult)
+      }
+    }
+
+    "preserve the existing failure for null reference-array elements" in {
+      intercept[IllegalArgumentException] {
+        HashCode.hash(HashCode.SEED, Array[AnyRef](null))
+      }.getMessage should include("Unexpected hash parameter: null")
+    }
+
+    "preserve recursive hashing for nested arrays" in {
+      val seed = HashCode.SEED
+      val afterNestedInts = fold(seed, Seq(1, 2, 3))
+      val expected = HashCode.hash(afterNestedInts, "tail")
+
+      HashCode.hash(seed, Array[AnyRef](Array(1, 2, 3), "tail")) should 
===(expected)
+    }
+  }
+}
diff --git a/actor/src/main/scala/org/apache/pekko/util/HashCode.scala 
b/actor/src/main/scala/org/apache/pekko/util/HashCode.scala
index fa52f1ff0f..ded3f0bba2 100644
--- a/actor/src/main/scala/org/apache/pekko/util/HashCode.scala
+++ b/actor/src/main/scala/org/apache/pekko/util/HashCode.scala
@@ -14,7 +14,6 @@
 package org.apache.pekko.util
 
 import java.lang.{ Double => JDouble, Float => JFloat }
-import java.lang.reflect.{ Array => JArray }
 
 /**
  * Set of methods which allow easy implementation of <code>hashCode</code>.
@@ -47,7 +46,7 @@ object HashCode {
       var result = seed
       if (value eq null) result = hash(result, 0)
       else if (!isArray(value)) result = hash(result, value.hashCode())
-      else for (id <- 0 until JArray.getLength(value)) result = hash(result, 
JArray.get(value, id)) // is an array
+      else result = hashArray(result, value)
       result
     case unexpected =>
       throw new IllegalArgumentException(s"Unexpected hash parameter: 
$unexpected") // will not happen, for exhaustiveness check
@@ -61,5 +60,67 @@ object HashCode {
 
   private def firstTerm(seed: Int): Int = PRIME * seed
   private def isArray(anyRef: AnyRef): Boolean = anyRef.getClass.isArray
+  private def hashArray(seed: Int, value: AnyRef): Int = {
+    var result = seed
+    value match {
+      case array: Array[AnyRef] =>
+        var index = 0
+        while (index < array.length) {
+          result = hash(result, array(index))
+          index += 1
+        }
+      case array: Array[Boolean] =>
+        var index = 0
+        while (index < array.length) {
+          result = hash(result, array(index))
+          index += 1
+        }
+      case array: Array[Char] =>
+        var index = 0
+        while (index < array.length) {
+          result = hash(result, array(index))
+          index += 1
+        }
+      case array: Array[Short] =>
+        var index = 0
+        while (index < array.length) {
+          result = hash(result, array(index))
+          index += 1
+        }
+      case array: Array[Int] =>
+        var index = 0
+        while (index < array.length) {
+          result = hash(result, array(index))
+          index += 1
+        }
+      case array: Array[Long] =>
+        var index = 0
+        while (index < array.length) {
+          result = hash(result, array(index))
+          index += 1
+        }
+      case array: Array[Float] =>
+        var index = 0
+        while (index < array.length) {
+          result = hash(result, array(index))
+          index += 1
+        }
+      case array: Array[Double] =>
+        var index = 0
+        while (index < array.length) {
+          result = hash(result, array(index))
+          index += 1
+        }
+      case array: Array[Byte] =>
+        var index = 0
+        while (index < array.length) {
+          result = hash(result, array(index))
+          index += 1
+        }
+      case unexpected =>
+        throw new IllegalArgumentException(s"Unexpected array hash parameter: 
$unexpected")
+    }
+    result
+  }
   private val PRIME = 37
 }
diff --git 
a/bench-jmh/src/main/scala/org/apache/pekko/util/HashCodeArrayBenchmark.scala 
b/bench-jmh/src/main/scala/org/apache/pekko/util/HashCodeArrayBenchmark.scala
new file mode 100644
index 0000000000..9b10bba701
--- /dev/null
+++ 
b/bench-jmh/src/main/scala/org/apache/pekko/util/HashCodeArrayBenchmark.scala
@@ -0,0 +1,46 @@
+/*
+ * 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.pekko.util
+
+import java.util.concurrent.TimeUnit
+
+import org.openjdk.jmh.annotations._
+
+@State(Scope.Benchmark)
+@BenchmarkMode(Array(Mode.AverageTime))
+@OutputTimeUnit(TimeUnit.NANOSECONDS)
+class HashCodeArrayBenchmark {
+
+  @Param(Array("16", "256"))
+  var size: Int = _
+
+  private var ints: Array[Int] = _
+  private var refs: Array[AnyRef] = _
+
+  @Setup
+  def setup(): Unit = {
+    ints = Array.tabulate(size)(identity)
+    refs = Array.tabulate[AnyRef](size)(index => Integer.valueOf(index))
+  }
+
+  @Benchmark
+  def hashIntArray(): Int = HashCode.hash(HashCode.SEED, ints)
+
+  @Benchmark
+  def hashReferenceArray(): Int = HashCode.hash(HashCode.SEED, refs)
+}


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

Reply via email to