Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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)
}
}
}
65 changes: 63 additions & 2 deletions actor/src/main/scala/org/apache/pekko/util/HashCode.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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>.
Expand Down Expand Up @@ -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
Expand All @@ -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
}
Original file line number Diff line number Diff line change
@@ -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)
}