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 T init(Supplier supplier) { + return supplier.get(); + } + + // strings used in hashing are set up ahead of time, so that the only allocation is from var-args + static String[] TEST_STRINGS = + init( + () -> { + ThreadLocalRandom random = ThreadLocalRandom.current(); + + String[] strings = new String[1024]; + for (int i = 0; i < strings.length; ++i) { + strings[i] = Double.toString(random.nextDouble()); + } + return strings; + }); + + static { + Thread updaterThread = + new Thread( + () -> { + ThreadLocalRandom random = ThreadLocalRandom.current(); + + while (!Thread.interrupted()) { + str0 = TEST_STRINGS[random.nextInt(0, TEST_STRINGS.length)]; + str1 = TEST_STRINGS[random.nextInt(0, TEST_STRINGS.length)]; + str2 = TEST_STRINGS[random.nextInt(0, TEST_STRINGS.length)]; + str3 = TEST_STRINGS[random.nextInt(0, TEST_STRINGS.length)]; + str4 = TEST_STRINGS[random.nextInt(0, TEST_STRINGS.length)]; + } + }); + updaterThread.setDaemon(true); + updaterThread.start(); + } + + static String str0; + static String str1; + static String str2; + static String str3; + static String str4; + + @Benchmark + public int hash2() { + return datadog.trace.util.HashingUtils.hash(str0, str1); + } + + @Benchmark + public int hash2_varargs() { + return java.util.Objects.hash(str0, str1); + } + + @Benchmark + public int hash3() { + return datadog.trace.util.HashingUtils.hash(str0, str1, str2); + } + + @Benchmark + public int hash3_varags() { + return java.util.Objects.hash(str0, str1, str2); + } + + @Benchmark + public int hash4() { + return datadog.trace.util.HashingUtils.hash(str0, str1, str2, str3); + } + + @Benchmark + public int hash4_varargs() { + return java.util.Objects.hash(str0, str1, str2, str3); + } + + @Benchmark + public int hash5() { + return datadog.trace.util.HashingUtils.hash(str0, str1, str2, str3, str4); + } + + @Benchmark + public int hash5_varargs() { + return java.util.Objects.hash(str0, str1, str2, str3, str4); + } +} diff --git a/internal-api/src/main/java/datadog/trace/util/HashingUtils.java b/internal-api/src/main/java/datadog/trace/util/HashingUtils.java new file mode 100644 index 00000000000..1522554836a --- /dev/null +++ b/internal-api/src/main/java/datadog/trace/util/HashingUtils.java @@ -0,0 +1,158 @@ +package datadog.trace.util; + +/** + * This class is intended to be a drop-in replacement for the hashing portions of java.util.Objects. + * This class provides more convenience methods for hashing primitives and includes overrides for + * 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; + } +}