diff --git a/internal-api/src/jmh/java/datadog/trace/util/HashingBenchmark.java b/internal-api/src/jmh/java/datadog/trace/util/HashingBenchmark.java new file mode 100644 index 00000000000..93a3d93e5f6 --- /dev/null +++ b/internal-api/src/jmh/java/datadog/trace/util/HashingBenchmark.java @@ -0,0 +1,135 @@ +package datadog.trace.util; + +import java.util.concurrent.ThreadLocalRandom; +import java.util.function.Supplier; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Threads; +import org.openjdk.jmh.annotations.Warmup; + +/** + * In contrast to java.util.Objects.hash, datadog.util.HashingUtils.hash has overrides for different + * parameter counts that allow most callers to avoid calling the var-arg version. This avoids the + * common situation where the JIT's escape analysis is unable to elide the var-arg array allocation. + * + *
This results in 3-4x throughput, but more importantly no allocation as compared to GiBs / sec
+ * with var-args.
+ * MacBook M1 using 8 threads/cores with -prof gc
+ *
+ * Benchmark Mode Cnt Score Error Units
+ *
+ * HashingBenchmark.hash2 thrpt 6 3365779949.250 ± 270198455.226 ops/s
+ * HashingBenchmark.hash2:gc.alloc.rate thrpt 6 0.001 ± 0.001 MB/sec
+ *
+ * HashingBenchmark.hash2_varargs thrpt 6 1194884232.767 ± 39724408.823 ops/s
+ * HashingBenchmark.hash2_varargs:gc.alloc.rate thrpt 6 27330.473 ± 909.029 MB/sec
+ *
+ *
+ * HashingBenchmark.hash3 thrpt 6 2314013984.714 ± 181952393.469 ops/s
+ * HashingBenchmark.hash3:gc.alloc.rate thrpt 6 0.001 ± 0.001 MB/sec
+ *
+ * HashingBenchmark.hash3_varags thrpt 6 869246242.250 ± 121680442.505 ops/s
+ * HashingBenchmark.hash3_varags:gc.alloc.rate thrpt 6 26514.569 ± 3709.819 MB/sec
+ *
+ *
+ * HashingBenchmark.hash4 thrpt 6 1866997193.226 ± 181198915.326 ops/s
+ * HashingBenchmark.hash4:gc.alloc.rate thrpt 6 0.001 ± 0.001 MB/sec
+ *
+ * HashingBenchmark.hash4_varargs thrpt 6 702697142.147 ± 24458612.481 ops/s
+ * HashingBenchmark.hash4_varargs:gc.alloc.rate thrpt 6 21437.996 ± 748.911 MB/sec
+ *
+ *
+ * HashingBenchmark.hash5 thrpt 6 1803117534.112 ± 242918817.144 ops/s
+ * HashingBenchmark.hash5:gc.alloc.rate thrpt 6 0.001 ± 0.001 MB/sec
+ *
+ * HashingBenchmark.hash5_varargs thrpt 6 579139583.196 ± 29525483.594 ops/s
+ * HashingBenchmark.hash5_varargs:gc.alloc.rate thrpt 6 22082.357 ± 1125.413 MB/sec
+ *
+ */
+@Fork(2)
+@Warmup(iterations = 2)
+@Measurement(iterations = 3)
+@Threads(8)
+public class HashingBenchmark {
+ static hash that take many argument lengths to avoid var-args allocation.
+ */
+public final class HashingUtils {
+ private HashingUtils() {}
+
+ public static final int hashCode(Object obj) {
+ return obj != null ? obj.hashCode() : 0;
+ }
+
+ public static final int hash(boolean value) {
+ return Boolean.hashCode(value);
+ }
+
+ public static final int hash(char value) {
+ return Character.hashCode(value);
+ }
+
+ public static final int hash(byte value) {
+ return Byte.hashCode(value);
+ }
+
+ public static final int hash(short value) {
+ return Short.hashCode(value);
+ }
+
+ public static final int hash(int value) {
+ return Integer.hashCode(value);
+ }
+
+ public static final int hash(long value) {
+ return Long.hashCode(value);
+ }
+
+ public static final int hash(float value) {
+ return Float.hashCode(value);
+ }
+
+ public static final int hash(double value) {
+ return Double.hashCode(value);
+ }
+
+ public static final int hash(Object obj) {
+ return obj != null ? obj.hashCode() : 0;
+ }
+
+ public static final int hash(Object obj0, Object obj1) {
+ return hash(hash(obj0), hash(obj1));
+ }
+
+ public static final int hash(int hash0, int hash1) {
+ return 31 * hash0 + hash1;
+ }
+
+ public static final int hash(Object obj0, Object obj1, Object obj2) {
+ return hash(hashCode(obj0), hashCode(obj1), hashCode(obj2));
+ }
+
+ public static final int hash(int hash0, int hash1, int hash2) {
+ // DQH - Micro-optimizing, 31 * 31 will constant fold
+ // Since there are multiple execution ports for load & store,
+ // this will make good use of the core.
+ return 31 * 31 * hash0 + 31 * hash1 + hash2;
+ }
+
+ public static final int hash(Object obj0, Object obj1, Object obj2, Object obj3) {
+ return hash(hashCode(obj0), hashCode(obj1), hashCode(obj2), hashCode(obj3));
+ }
+
+ public static final int hash(int hash0, int hash1, int hash2, int hash3) {
+ // DQH - Micro-optimizing, 31 * 31 will constant fold
+ // Since there are multiple execution ports for load & store,
+ // this will make good use of the core.
+ return 31 * 31 * 31 * hash0 + 31 * 31 * hash1 + 31 * hash2 + hash3;
+ }
+
+ public static final int hash(Object obj0, Object obj1, Object obj2, Object obj3, Object obj4) {
+ return hash(hashCode(obj0), hashCode(obj1), hashCode(obj2), hashCode(obj3));
+ }
+
+ public static final int hash(int hash0, int hash1, int hash2, int hash3, int hash4) {
+ // DQH - Micro-optimizing, 31 * 31 will constant fold
+ // Since there are multiple execution ports for load & store,
+ // this will make good use of the core.
+ return 31 * 31 * 31 * 31 * hash0 + 31 * 31 * 31 * hash1 + 31 * 31 * hash2 + 31 * hash3 + hash4;
+ }
+
+ @Deprecated
+ public static final int hash(int[] hashes) {
+ int result = 0;
+ for (int hash : hashes) {
+ result = addToHash(result, hash);
+ }
+ return result;
+ }
+
+ public static final int addToHash(int hash, int value) {
+ return 31 * hash + value;
+ }
+
+ public static final int addToHash(int hash, Object obj) {
+ return addToHash(hash, hashCode(obj));
+ }
+
+ public static final int addToHash(int hash, boolean value) {
+ return addToHash(hash, Boolean.hashCode(value));
+ }
+
+ public static final int addToHash(int hash, char value) {
+ return addToHash(hash, Character.hashCode(value));
+ }
+
+ public static final int addToHash(int hash, byte value) {
+ return addToHash(hash, Byte.hashCode(value));
+ }
+
+ public static final int addToHash(int hash, short value) {
+ return addToHash(hash, Short.hashCode(value));
+ }
+
+ public static final int addToHash(int hash, long value) {
+ return addToHash(hash, Long.hashCode(value));
+ }
+
+ public static final int addToHash(int hash, float value) {
+ return addToHash(hash, Float.hashCode(value));
+ }
+
+ public static final int addToHash(int hash, double value) {
+ return addToHash(hash, Double.hashCode(value));
+ }
+
+ public static final int hash(Iterable> objs) {
+ int result = 0;
+ for (Object obj : objs) {
+ result = addToHash(result, obj);
+ }
+ return result;
+ }
+
+ /**
+ * Calling this var-arg version can result in large amounts of allocation (see HashingBenchmark)
+ * Rather than calliing this method, add another override of hash that handles a larger number of
+ * arguments or use calls to addToHash.
+ */
+ @Deprecated
+ public static final int hash(Object[] objs) {
+ int result = 0;
+ for (Object obj : objs) {
+ result = addToHash(result, obj);
+ }
+ return result;
+ }
+}
diff --git a/internal-api/src/test/java/datadog/trace/util/HashingUtilsTest.java b/internal-api/src/test/java/datadog/trace/util/HashingUtilsTest.java
new file mode 100644
index 00000000000..185d5a4f2e4
--- /dev/null
+++ b/internal-api/src/test/java/datadog/trace/util/HashingUtilsTest.java
@@ -0,0 +1,244 @@
+package datadog.trace.util;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+
+import java.util.Arrays;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+public class HashingUtilsTest {
+ @Test
+ public void hashCode_() {
+ assertEquals("bar".hashCode(), HashingUtils.hashCode("bar"));
+ }
+
+ @Test
+ public void hashCodeNull() {
+ assertEquals(0, HashingUtils.hash((Object) null));
+ }
+
+ @Test
+ public void hash1() {
+ assertEquals("foo".hashCode(), HashingUtils.hashCode("foo"));
+ }
+
+ @Test
+ public void hash1Null() {
+ assertEquals(0, HashingUtils.hashCode(null));
+ }
+
+ @Test
+ public void hash2() {
+ String str0 = "foo";
+ String str1 = "bar";
+
+ assertNotEquals(0, HashingUtils.hash(str0, str1));
+
+ String clone0 = clone(str0);
+ String clone1 = clone(str1);
+
+ assertEquals(HashingUtils.hash(str0, str1), HashingUtils.hash(clone0, clone1));
+ }
+
+ @Test
+ public void hash2Null() {
+ assertEquals(0, HashingUtils.hash(null, null));
+ }
+
+ @Test
+ public void hash3() {
+ String str0 = "foo";
+ String str2 = "quux";
+ String str1 = "bar";
+
+ assertNotEquals(0, HashingUtils.hash(str0, str1, str2));
+
+ String clone0 = clone(str0);
+ String clone1 = clone(str1);
+ String clone2 = clone(str2);
+
+ assertEquals(HashingUtils.hash(str0, str1, str2), HashingUtils.hash(clone0, clone1, clone2));
+ }
+
+ @Test
+ public void hash3Null() {
+ assertEquals(0, HashingUtils.hash(null, null, null));
+ }
+
+ @Test
+ public void hash4() {
+ String str0 = "foo";
+ String str1 = "bar";
+ String str2 = "quux";
+ String str3 = "foobar";
+
+ assertNotEquals(0, HashingUtils.hash(str0, str1, str2, str3));
+
+ String clone0 = clone(str0);
+ String clone1 = clone(str1);
+ String clone2 = clone(str2);
+ String clone3 = clone(str3);
+
+ assertEquals(
+ HashingUtils.hash(str0, str1, str2, str3),
+ HashingUtils.hash(clone0, clone1, clone2, clone3));
+ }
+
+ @Test
+ public void hash4Null() {
+ assertEquals(0, HashingUtils.hash(null, null, null, null));
+ }
+
+ @Test
+ public void hash5() {
+ String str0 = "foo";
+ String str1 = "bar";
+ String str2 = "quux";
+ String str3 = "foobar";
+ String str4 = "hello";
+
+ assertNotEquals(0, HashingUtils.hash(str0, str1, str2, str3));
+
+ String clone0 = clone(str0);
+ String clone1 = clone(str1);
+ String clone2 = clone(str2);
+ String clone3 = clone(str3);
+ String clone4 = clone(str4);
+
+ assertEquals(
+ HashingUtils.hash(str0, str1, str2, str3, str4),
+ HashingUtils.hash(clone0, clone1, clone2, clone3, clone4));
+ }
+
+ @Test
+ public void hash5Null() {
+ assertEquals(0, HashingUtils.hash(null, null, null, null, null));
+ }
+
+ @Test
+ public void hashArrayAndIterable() {
+ String str0 = "foo";
+ String str1 = "bar";
+ String str2 = "quux";
+ String str3 = "foobar";
+ String str4 = "foobaz";
+ String str5 = "hello";
+ String str6 = "world";
+
+ Object[] array = new Object[] {str0, str1, str2, str3, str4, str5, str6};
+
+ int hashArray = HashingUtils.hash(array);
+ assertNotEquals(0, hashArray);
+
+ int hashIterable = HashingUtils.hash(Arrays.asList(array));
+ assertNotEquals(0, hashIterable);
+
+ assertEquals(hashArray, hashIterable);
+ }
+
+ @ParameterizedTest
+ @ValueSource(booleans = {false, true})
+ public void booleans(boolean value) {
+ assertEquals(Boolean.hashCode(value), HashingUtils.hash(value));
+ assertEquals(Boolean.hashCode(value), HashingUtils.addToHash(0, value));
+ }
+
+ @ParameterizedTest
+ @ValueSource(chars = {Character.MIN_VALUE, 'a', 'A', '\0', 'z', 'Z', Character.MAX_VALUE})
+ public void chars(char value) {
+ assertEquals(Character.hashCode(value), HashingUtils.hash(value));
+ assertEquals(Character.hashCode(value), HashingUtils.addToHash(0, value));
+ }
+
+ @ParameterizedTest
+ @ValueSource(bytes = {Byte.MIN_VALUE, -1, 0, 1, Byte.MAX_VALUE})
+ public void bytes(byte value) {
+ assertEquals(Byte.hashCode(value), HashingUtils.hash(value));
+ assertEquals(Byte.hashCode(value), HashingUtils.addToHash(0, value));
+ }
+
+ @ParameterizedTest
+ @ValueSource(
+ shorts = {Short.MIN_VALUE, Byte.MIN_VALUE, -1, 0, 1, Byte.MAX_VALUE, Short.MAX_VALUE})
+ public void shorts(short value) {
+ assertEquals(Short.hashCode(value), HashingUtils.hash(value));
+ assertEquals(Short.hashCode(value), HashingUtils.addToHash(0, value));
+ }
+
+ @ParameterizedTest
+ @ValueSource(
+ ints = {
+ Integer.MIN_VALUE,
+ Short.MIN_VALUE,
+ Byte.MIN_VALUE,
+ -1,
+ 0,
+ 1,
+ Byte.MAX_VALUE,
+ Short.MAX_VALUE,
+ Integer.MAX_VALUE
+ })
+ public void ints(int value) {
+ assertEquals(Integer.hashCode(value), HashingUtils.hash(value));
+ assertEquals(Integer.hashCode(value), HashingUtils.addToHash(0, value));
+ }
+
+ @ParameterizedTest
+ @ValueSource(
+ longs = {
+ Long.MIN_VALUE,
+ Integer.MIN_VALUE,
+ Short.MIN_VALUE,
+ Byte.MIN_VALUE,
+ -1,
+ 0,
+ 1,
+ Byte.MAX_VALUE,
+ Short.MAX_VALUE,
+ Integer.MAX_VALUE,
+ Long.MAX_VALUE
+ })
+ public void longs(long value) {
+ assertEquals(Long.hashCode(value), HashingUtils.hash(value));
+ assertEquals(Long.hashCode(value), HashingUtils.addToHash(0, value));
+ }
+
+ @ParameterizedTest
+ @ValueSource(floats = {Float.MIN_VALUE, -1, 0, 1, 2.71828f, 3.1415f, Float.MAX_VALUE})
+ public void floats(float value) {
+ assertEquals(Float.hashCode(value), HashingUtils.hash(value));
+ assertEquals(Float.hashCode(value), HashingUtils.addToHash(0, value));
+ }
+
+ @ParameterizedTest
+ @ValueSource(
+ doubles = {
+ Double.MIN_VALUE,
+ Float.MIN_VALUE,
+ -1,
+ 0,
+ 1,
+ 2.71828,
+ 3.1415,
+ Float.MAX_VALUE,
+ Double.MAX_VALUE
+ })
+ public void floats(double value) {
+ assertEquals(Double.hashCode(value), HashingUtils.hash(value));
+ assertEquals(Double.hashCode(value), HashingUtils.addToHash(0, value));
+ }
+
+ static final String clone(String str) {
+ return new String(str);
+ }
+
+ static final String[] deepClone(String[] strings) {
+ String[] clones = new String[strings.length];
+ for (int i = 0; i < strings.length; ++i) {
+ clones[i] = clone(strings[i]);
+ }
+ return clones;
+ }
+}