Skip to content

Commit afdf801

Browse files
dougqhdevflow.devflow-routing-intake
andauthored
Add Hashtable and LongHashingUtils utilities (#11409)
Add Hashtable and LongHashingUtils to datadog.trace.util Two general-purpose utilities used by the client-side stats aggregator work (PR #11382 and follow-ups), extracted into their own change so the metrics-specific PRs can build on a smaller, reviewable foundation. - Hashtable: a generic open-addressed-ish bucket table abstraction keyed by a 64-bit hash, with a public abstract Entry type so client code can subclass it for higher-arity keys. The metrics aggregator uses it to back its AggregateTable. - LongHashingUtils: chained 64-bit hash combiners with primitive overloads (boolean, short, int, long, Object). Used in place of varargs combiners to avoid Object[] allocation and boxing on the hot path. No callers within internal-api itself yet -- the metrics aggregator PR will introduce the first usages. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Add unit tests for Hashtable and LongHashingUtils LongHashingUtilsTest (14 cases): - hashCodeX null sentinel + non-null pass-through - all primitive hash() overloads match the boxed Java hashCodes - hash(Object...) 2/3/4/5-arg overloads match the chained addToHash formula they are documented to constant-fold to - addToHash(long, primitive) overloads match the Object-version - linear-accumulation invariant (31 * h + v) holds across a sequence - iterable / deprecated int[] / deprecated Object[] variants match chained addToHash - intHash treats null as 0 (observable via hash(null, "x")) HashtableTest (24 cases across 5 nested classes): - D1: insert/get/remove/insertOrReplace/clear/forEach, in-place value mutation, null-key handling, hash-collision chaining with disambig- uating equals, remove-from-collided-chain leaves siblings intact - D2: pair-key identity, remove(pair), insertOrReplace matches on both parts, forEach - Support: capacity rounds up to a power of two, bucketIndex stays in range across a wide hash sample, clear nulls every slot - BucketIterator: walks only matching-hash entries in a chain, throws NoSuchElementException when exhausted - MutatingBucketIterator: remove from head-of-chain unlinks, replace swaps the entry while preserving chain, remove() without prior next() throws IllegalStateException Tests live in internal-api/src/test/java/datadog/trace/util and use the already-present JUnit 5 setup. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Apply spotless formatting to Hashtable and LongHashingUtils Bring the new util/ files in line with google-java-format (tabs → spaces, line wrapping, javadoc list markup) so spotlessCheck passes in CI. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Add JMH benchmarks for Hashtable.D1 and D2 Compares Hashtable.D1 and Hashtable.D2 against equivalent HashMap usage for add, update, and iterate operations. Each benchmark thread owns its own map (Scope.Thread), but @threads(8) is used so the allocation/GC pressure that Hashtable is designed to avoid surfaces in the throughput numbers. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Add benchmark results to HashtableBenchmark header Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Address review feedback on Hashtable - Guard Support.sizeFor against overflow and use Integer.highestOneBit; reject capacities above 1 << 30 instead of looping forever. - Add braces around single-statement while bodies in BucketIterator. - Split HashtableBenchmark into HashtableD1Benchmark / HashtableD2Benchmark. - Add regression tests for Support.sizeFor bounds. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Fix dropped argument in HashingUtils 5-arg Object hash The 5-arg Object overload was forwarding only obj0..obj3 to the int overload, silently dropping obj4. Also align LongHashingUtils.hash 3-arg signature with its 2/4/5-arg siblings (int parameters) and strengthen the 5-arg HashingUtilsTest to detect the missing-arg regression. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Address review feedback on Hashtable - Split D1Tests and D2Tests into HashtableD1Test and HashtableD2Test; extract shared test entry classes into HashtableTestEntries. - Reduce visibility of LongHashingUtils.hash(int...) chaining overloads to package-private; they are internal building blocks. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Drop reflection in iterator tests via package-private D1.buckets The iterator tests need a populated Hashtable.Entry[] to drive Support.bucketIterator / mutatingBucketIterator. Relaxing D1.buckets from private to package-private lets the same-package tests read it directly, removing the reflection helper. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Add context-passing forEach to Hashtable.D1 and D2 Mirrors the TagMap pattern: pairs the existing forEach(Consumer) with a forEach(T context, BiConsumer<T, TEntry>) overload so callers can hand side-band state to a non-capturing lambda and avoid the fresh-Consumer-per-call allocation. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Move forEach loop body to Support helper Factors the unchecked (TEntry) cast out of D1.forEach / D2.forEach (and the BiConsumer variants) into Support.forEach(buckets, ...). The cast now lives in one place, mirroring how Entry.next() handles it, and the D1/D2 methods become one-liners. Downstream higher-arity tables built on Support gain the same helper. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Move bucket-head cast to Support.bucket helper Adds Support.bucket(buckets, keyHash) which returns the bucket head already cast to the caller's concrete entry type. D1.get and D2.get now drop the raw-Entry intermediate variable and walk the chain via Entry.next() directly. The unchecked cast lives in one place, consistent with Entry.next() and Support.forEach. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Drop d1_/d2_ prefix from per-table benchmark methods Holdover from when both lived in a shared HashtableBenchmark; redundant now that each lives in its own class. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Add Hashtable.Support helpers: MAX_RATIO, insertHeadEntry, MutatingTableIterator Three consumer-facing helpers that callers building higher-arity tables on top of Hashtable.Support kept open-coding: - MAX_RATIO_NUMERATOR / _DENOMINATOR: the 4/3 multiplier for sizing a bucket array from a target working-set under a 75% load factor. - insertHeadEntry(buckets, bucketIndex, entry): the (setNext + array-store) pair for splicing a new entry at the head of a bucket chain. - MutatingTableIterator + Support.mutatingTableIterator(buckets): walks every entry in the table (not filtered by hash) with remove() support, for sweeps like eviction and expunge that aren't keyed to a specific hash. Sibling of MutatingBucketIterator. Tests cover the table-wide iterator at head-of-bucket and mid-chain removal, empty buckets between live entries, exhaustion, and remove-without-next. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Swap MAX_RATIO numerator/denominator pair for a single float + scaled create() Replace Support.MAX_RATIO_NUMERATOR / _DENOMINATOR with a single float MAX_RATIO constant, and add a Support.create(int, float) overload that takes a scale factor. Callers now write Support.create(n, MAX_RATIO) instead of stitching together the int arithmetic at the call site. The scaled size is truncated (not ceiled) before going through sizeFor. sizeFor already rounds up to the next power of two, so truncation just absorbs float fuzz that would otherwise push a result like 12 * 4/3 = 16.0000005f past 16 and double the bucket array size for no reason. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Tighten Hashtable docs + rename MAX_CAPACITY to MAX_BUCKETS Five small cleanups from a design re-review pass: 1. Support javadoc: drop the stale "methods are package-private" sentence; most of them were made public in earlier commits for higher-arity callers. Also drop the "nested BucketIterator" framing (iterators are peers of Support inside Hashtable, not nested inside Support). 2. MAX_RATIO javadoc: drop the Math.ceil recommendation; create(int, float) deliberately truncates and is the canonical pathway. 3. Document the null-hash treatment on D1.Entry.hash and D2.Entry.hash so the behavior difference is explicit: D1 uses Long.MIN_VALUE as a sentinel that's collision-free against any int-valued hashCode(); D2 has no such sentinel and relies on matches() to resolve null/null vs hash-0 collisions. 4. Rename Support.MAX_CAPACITY -> MAX_BUCKETS and sizeFor's parameter to requestedSize. The cap is on the bucket-array length, not entry count; the new name reflects that. Error messages updated to match. 5. Drop the `abstract` modifier on Hashtable in favor of `final` with a private constructor. Nothing actually subclasses Hashtable -- the abstract was a namespace device that read as "intended for extension." Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Dedupe chain-head splice in D1/D2 via keyHash insertHeadEntry overload - Add Support.insertHeadEntry(buckets, long keyHash, entry) overload that derives the bucket index itself. Callers that already have a hash but not the index (the common case) now avoid the redundant bucketIndex(...) hop. - D1.insert, D1.insertOrReplace, D2.insert, D2.insertOrReplace: use the new overload, drop the (thisBuckets local, bucketIndex compute, setNext, store) sequence at each call site. - D2.buckets: drop the `private` modifier to match D1.buckets. Both are package-private so iterator tests in the same package can drive Support.bucketIterator against the table's bucket array. Added a short comment on both fields documenting the rationale. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Tighten Entry.next encapsulation; doc hasNext; add D1/D2 getOrCreate Three follow-ups from the design review: - Make Hashtable.Entry.next private. All same-package readers (BucketIterator) already had a next() accessor; the leftover direct field reads now route through it. Closes the "mixed encapsulation" gap where some readers used the accessor and same-package ones reached for the field. - BucketIterator and MutatingBucketIterator now document that chain-walk work happens in next() (and the constructor for the first match); hasNext() is an O(1) field read. - Add D1.getOrCreate(K, Function) and D2.getOrCreate(K1, K2, BiFunction). Both reuse the lookup hash for the insert on miss, avoiding the double-hash that "get; if null then insert" callers would otherwise pay. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Hashtable: add missing braces and detach removed/replaced entries Addresses PR #11409 review comments: - #3267164119 / #3267165525: wrap every single-line if/break body in braces (7 sites across BucketIterator, MutatingBucketIterator, and the full-table Iterator). - #3275947761 / #3275948108 (sarahchen6): null out the removed/replaced entry's next pointer after splicing it out of the chain in MutatingBucketIterator.remove / .replace. Applied the same fix to the full-table Iterator.remove for consistency. Rationale: detaching prevents accidental traversal through a removed entry via a stale reference and lets the GC reclaim a chain tail that the removed entry was the last referrer to. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Rename LongHashingUtils.hashCodeX(Object) to hash(Object) for API consistency Addresses PR #11409 review comment #3276167001. The method parallels the primitive hash(boolean) / hash(int) / hash(long) / ... family, so naming it hash(Object) -- with null collapsing to Long.MIN_VALUE as a sentinel distinct from any real hashCode -- matches the rest of the public surface. Test call sites that pass a literal null now disambiguate against hash(int[]) / hash(Object[]) / hash(Iterable) via an (Object) cast. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Merge branch 'master' into dougqh/util-hashtable Co-authored-by: devflow.devflow-routing-intake <devflow.devflow-routing-intake@kubernetes.us1.ddbuild.io>
1 parent f7a0a44 commit afdf801

11 files changed

Lines changed: 2339 additions & 2 deletions

File tree

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
package datadog.trace.util;
2+
3+
import static java.util.concurrent.TimeUnit.MICROSECONDS;
4+
5+
import java.util.HashMap;
6+
import java.util.Map;
7+
import java.util.function.Consumer;
8+
import org.openjdk.jmh.annotations.Benchmark;
9+
import org.openjdk.jmh.annotations.BenchmarkMode;
10+
import org.openjdk.jmh.annotations.Fork;
11+
import org.openjdk.jmh.annotations.Level;
12+
import org.openjdk.jmh.annotations.Measurement;
13+
import org.openjdk.jmh.annotations.Mode;
14+
import org.openjdk.jmh.annotations.OperationsPerInvocation;
15+
import org.openjdk.jmh.annotations.OutputTimeUnit;
16+
import org.openjdk.jmh.annotations.Scope;
17+
import org.openjdk.jmh.annotations.Setup;
18+
import org.openjdk.jmh.annotations.State;
19+
import org.openjdk.jmh.annotations.Threads;
20+
import org.openjdk.jmh.annotations.Warmup;
21+
import org.openjdk.jmh.infra.Blackhole;
22+
23+
/**
24+
* Compares {@link Hashtable.D1} against equivalent {@link HashMap} usage for add, update, and
25+
* iterate operations.
26+
*
27+
* <p>Each benchmark thread owns its own map ({@link Scope#Thread}), but a non-trivial thread count
28+
* is used so allocation/GC pressure surfaces in the throughput numbers — that pressure is the main
29+
* thing Hashtable is built to avoid.
30+
*
31+
* <ul>
32+
* <li><b>add</b> — clear the map then re-insert N fresh entries
33+
* ({@code @OperationsPerInvocation(N_KEYS)}). Captures the steady-state cost of building up a
34+
* map.
35+
* <li><b>update</b> — for an existing key, increment a counter. Hashtable does {@code get} +
36+
* field mutation (no allocation); HashMap uses {@code merge(k, 1L, Long::sum)}, the idiomatic
37+
* Java 8+ way, which still allocates a {@code Long} per call.
38+
* <li><b>iterate</b> — walk every entry and consume its key + value.
39+
* </ul>
40+
*
41+
* <p><b>Update</b> is where Hashtable dominates: D1 is ~14x faster, because the HashMap path
42+
* allocates per call (a {@code Long}) and the resulting GC pressure throttles throughput under
43+
* multiple threads. <b>Add</b> is roughly comparable (both allocate one entry per insert).
44+
* <b>Iterate</b> is essentially a wash — both are bucket walks. <code>
45+
* MacBook M1 8 threads (Java 8)
46+
*
47+
* Benchmark Mode Cnt Score Error Units
48+
* HashtableD1Benchmark.add_hashMap thrpt 6 187.883 ± 189.858 ops/us
49+
* HashtableD1Benchmark.add_hashtable thrpt 6 198.710 ± 273.035 ops/us
50+
*
51+
* HashtableD1Benchmark.update_hashMap thrpt 6 127.392 ± 87.482 ops/us
52+
* HashtableD1Benchmark.update_hashtable thrpt 6 1810.244 ± 44.645 ops/us
53+
*
54+
* HashtableD1Benchmark.iterate_hashMap thrpt 6 20.043 ± 0.752 ops/us
55+
* HashtableD1Benchmark.iterate_hashtable thrpt 6 22.208 ± 0.956 ops/us
56+
* </code>
57+
*/
58+
@Fork(2)
59+
@Warmup(iterations = 2)
60+
@Measurement(iterations = 3)
61+
@BenchmarkMode(Mode.Throughput)
62+
@OutputTimeUnit(MICROSECONDS)
63+
@Threads(8)
64+
public class HashtableD1Benchmark {
65+
66+
static final int N_KEYS = 64;
67+
static final int CAPACITY = 128;
68+
69+
static final String[] SOURCE_KEYS = new String[N_KEYS];
70+
71+
static {
72+
for (int i = 0; i < N_KEYS; ++i) {
73+
SOURCE_KEYS[i] = "key-" + i;
74+
}
75+
}
76+
77+
static final class D1Counter extends Hashtable.D1.Entry<String> {
78+
long count;
79+
80+
D1Counter(String key) {
81+
super(key);
82+
}
83+
}
84+
85+
/** Reusable iteration consumer — avoids per-call lambda capture allocation. */
86+
static final class BhD1Consumer implements Consumer<D1Counter> {
87+
Blackhole bh;
88+
89+
@Override
90+
public void accept(D1Counter e) {
91+
bh.consume(e.key);
92+
bh.consume(e.count);
93+
}
94+
}
95+
96+
@State(Scope.Thread)
97+
public static class D1State {
98+
Hashtable.D1<String, D1Counter> table;
99+
HashMap<String, Long> hashMap;
100+
String[] keys;
101+
int cursor;
102+
final BhD1Consumer consumer = new BhD1Consumer();
103+
104+
@Setup(Level.Iteration)
105+
public void setUp() {
106+
table = new Hashtable.D1<>(CAPACITY);
107+
hashMap = new HashMap<>(CAPACITY);
108+
keys = SOURCE_KEYS;
109+
for (int i = 0; i < N_KEYS; ++i) {
110+
table.insert(new D1Counter(keys[i]));
111+
hashMap.put(keys[i], 0L);
112+
}
113+
cursor = 0;
114+
}
115+
116+
String nextKey() {
117+
int i = cursor;
118+
cursor = (i + 1) & (N_KEYS - 1);
119+
return keys[i];
120+
}
121+
}
122+
123+
@Benchmark
124+
@OperationsPerInvocation(N_KEYS)
125+
public void add_hashtable(D1State s) {
126+
Hashtable.D1<String, D1Counter> t = s.table;
127+
String[] keys = s.keys;
128+
t.clear();
129+
for (int i = 0; i < N_KEYS; ++i) {
130+
t.insert(new D1Counter(keys[i]));
131+
}
132+
}
133+
134+
@Benchmark
135+
@OperationsPerInvocation(N_KEYS)
136+
public void add_hashMap(D1State s) {
137+
HashMap<String, Long> m = s.hashMap;
138+
String[] keys = s.keys;
139+
m.clear();
140+
for (int i = 0; i < N_KEYS; ++i) {
141+
m.put(keys[i], (long) i);
142+
}
143+
}
144+
145+
@Benchmark
146+
public long update_hashtable(D1State s) {
147+
D1Counter e = s.table.get(s.nextKey());
148+
return ++e.count;
149+
}
150+
151+
@Benchmark
152+
public Long update_hashMap(D1State s) {
153+
return s.hashMap.merge(s.nextKey(), 1L, Long::sum);
154+
}
155+
156+
@Benchmark
157+
public void iterate_hashtable(D1State s, Blackhole bh) {
158+
s.consumer.bh = bh;
159+
s.table.forEach(s.consumer);
160+
}
161+
162+
@Benchmark
163+
public void iterate_hashMap(D1State s, Blackhole bh) {
164+
for (Map.Entry<String, Long> entry : s.hashMap.entrySet()) {
165+
bh.consume(entry.getKey());
166+
bh.consume(entry.getValue());
167+
}
168+
}
169+
}
Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
package datadog.trace.util;
2+
3+
import static java.util.concurrent.TimeUnit.MICROSECONDS;
4+
5+
import java.util.HashMap;
6+
import java.util.Map;
7+
import java.util.Objects;
8+
import java.util.function.Consumer;
9+
import org.openjdk.jmh.annotations.Benchmark;
10+
import org.openjdk.jmh.annotations.BenchmarkMode;
11+
import org.openjdk.jmh.annotations.Fork;
12+
import org.openjdk.jmh.annotations.Level;
13+
import org.openjdk.jmh.annotations.Measurement;
14+
import org.openjdk.jmh.annotations.Mode;
15+
import org.openjdk.jmh.annotations.OperationsPerInvocation;
16+
import org.openjdk.jmh.annotations.OutputTimeUnit;
17+
import org.openjdk.jmh.annotations.Scope;
18+
import org.openjdk.jmh.annotations.Setup;
19+
import org.openjdk.jmh.annotations.State;
20+
import org.openjdk.jmh.annotations.Threads;
21+
import org.openjdk.jmh.annotations.Warmup;
22+
import org.openjdk.jmh.infra.Blackhole;
23+
24+
/**
25+
* Compares {@link Hashtable.D2} against equivalent {@link HashMap} usage for add, update, and
26+
* iterate operations.
27+
*
28+
* <p>Each benchmark thread owns its own map ({@link Scope#Thread}), but a non-trivial thread count
29+
* is used so allocation/GC pressure surfaces in the throughput numbers — that pressure is the main
30+
* thing Hashtable is built to avoid.
31+
*
32+
* <ul>
33+
* <li><b>add</b> — clear the map then re-insert N fresh entries
34+
* ({@code @OperationsPerInvocation(N_KEYS)}). Captures the steady-state cost of building up a
35+
* map.
36+
* <li><b>update</b> — for an existing key, increment a counter. Hashtable does {@code get} +
37+
* field mutation (no allocation); HashMap uses {@code merge(k, 1L, Long::sum)}, the idiomatic
38+
* Java 8+ way, which still allocates a {@code Long} per call.
39+
* <li><b>iterate</b> — walk every entry and consume its key + value.
40+
* </ul>
41+
*
42+
* <p>The D2 variants additionally pay for a composite-key wrapper allocation in the HashMap path
43+
* (Java has no built-in tuple-as-key) — D2 sidesteps it by taking both key parts directly.
44+
*
45+
* <p><b>Update</b> is where Hashtable dominates: D2 is ~26x faster, because the HashMap path
46+
* allocates per call (a {@code Long}, plus a {@code Key2}) and the resulting GC pressure throttles
47+
* throughput under multiple threads. <b>Add</b> is ~3x faster for D2 (Hashtable sidesteps the
48+
* {@code Key2} allocation). <b>Iterate</b> is essentially a wash — both are bucket walks. <code>
49+
* MacBook M1 8 threads (Java 8)
50+
*
51+
* Benchmark Mode Cnt Score Error Units
52+
* HashtableD2Benchmark.add_hashMap thrpt 6 77.082 ± 72.278 ops/us
53+
* HashtableD2Benchmark.add_hashtable thrpt 6 216.813 ± 413.236 ops/us
54+
*
55+
* HashtableD2Benchmark.update_hashMap thrpt 6 56.077 ± 23.716 ops/us
56+
* HashtableD2Benchmark.update_hashtable thrpt 6 1445.868 ± 157.705 ops/us
57+
*
58+
* HashtableD2Benchmark.iterate_hashMap thrpt 6 19.508 ± 0.760 ops/us
59+
* HashtableD2Benchmark.iterate_hashtable thrpt 6 16.968 ± 0.371 ops/us
60+
* </code>
61+
*/
62+
@Fork(2)
63+
@Warmup(iterations = 2)
64+
@Measurement(iterations = 3)
65+
@BenchmarkMode(Mode.Throughput)
66+
@OutputTimeUnit(MICROSECONDS)
67+
@Threads(8)
68+
public class HashtableD2Benchmark {
69+
70+
static final int N_KEYS = 64;
71+
static final int CAPACITY = 128;
72+
73+
static final String[] SOURCE_K1 = new String[N_KEYS];
74+
static final Integer[] SOURCE_K2 = new Integer[N_KEYS];
75+
76+
static {
77+
for (int i = 0; i < N_KEYS; ++i) {
78+
SOURCE_K1[i] = "key-" + i;
79+
SOURCE_K2[i] = i * 31 + 17;
80+
}
81+
}
82+
83+
static final class D2Counter extends Hashtable.D2.Entry<String, Integer> {
84+
long count;
85+
86+
D2Counter(String k1, Integer k2) {
87+
super(k1, k2);
88+
}
89+
}
90+
91+
/** Composite key for the HashMap baseline against D2. */
92+
static final class Key2 {
93+
final String k1;
94+
final Integer k2;
95+
final int hash;
96+
97+
Key2(String k1, Integer k2) {
98+
this.k1 = k1;
99+
this.k2 = k2;
100+
this.hash = Objects.hash(k1, k2);
101+
}
102+
103+
@Override
104+
public boolean equals(Object o) {
105+
if (!(o instanceof Key2)) {
106+
return false;
107+
}
108+
Key2 other = (Key2) o;
109+
return Objects.equals(k1, other.k1) && Objects.equals(k2, other.k2);
110+
}
111+
112+
@Override
113+
public int hashCode() {
114+
return hash;
115+
}
116+
}
117+
118+
/** Reusable iteration consumer — avoids per-call lambda capture allocation. */
119+
static final class BhD2Consumer implements Consumer<D2Counter> {
120+
Blackhole bh;
121+
122+
@Override
123+
public void accept(D2Counter e) {
124+
bh.consume(e.key1);
125+
bh.consume(e.key2);
126+
bh.consume(e.count);
127+
}
128+
}
129+
130+
@State(Scope.Thread)
131+
public static class D2State {
132+
Hashtable.D2<String, Integer, D2Counter> table;
133+
HashMap<Key2, Long> hashMap;
134+
String[] k1s;
135+
Integer[] k2s;
136+
int cursor;
137+
final BhD2Consumer consumer = new BhD2Consumer();
138+
139+
@Setup(Level.Iteration)
140+
public void setUp() {
141+
table = new Hashtable.D2<>(CAPACITY);
142+
hashMap = new HashMap<>(CAPACITY);
143+
k1s = SOURCE_K1;
144+
k2s = SOURCE_K2;
145+
for (int i = 0; i < N_KEYS; ++i) {
146+
table.insert(new D2Counter(k1s[i], k2s[i]));
147+
hashMap.put(new Key2(k1s[i], k2s[i]), 0L);
148+
}
149+
cursor = 0;
150+
}
151+
152+
int nextIndex() {
153+
int i = cursor;
154+
cursor = (i + 1) & (N_KEYS - 1);
155+
return i;
156+
}
157+
}
158+
159+
@Benchmark
160+
@OperationsPerInvocation(N_KEYS)
161+
public void add_hashtable(D2State s) {
162+
Hashtable.D2<String, Integer, D2Counter> t = s.table;
163+
String[] k1s = s.k1s;
164+
Integer[] k2s = s.k2s;
165+
t.clear();
166+
for (int i = 0; i < N_KEYS; ++i) {
167+
t.insert(new D2Counter(k1s[i], k2s[i]));
168+
}
169+
}
170+
171+
@Benchmark
172+
@OperationsPerInvocation(N_KEYS)
173+
public void add_hashMap(D2State s) {
174+
HashMap<Key2, Long> m = s.hashMap;
175+
String[] k1s = s.k1s;
176+
Integer[] k2s = s.k2s;
177+
m.clear();
178+
for (int i = 0; i < N_KEYS; ++i) {
179+
m.put(new Key2(k1s[i], k2s[i]), (long) i);
180+
}
181+
}
182+
183+
@Benchmark
184+
public long update_hashtable(D2State s) {
185+
int i = s.nextIndex();
186+
D2Counter e = s.table.get(s.k1s[i], s.k2s[i]);
187+
return ++e.count;
188+
}
189+
190+
@Benchmark
191+
public Long update_hashMap(D2State s) {
192+
int i = s.nextIndex();
193+
return s.hashMap.merge(new Key2(s.k1s[i], s.k2s[i]), 1L, Long::sum);
194+
}
195+
196+
@Benchmark
197+
public void iterate_hashtable(D2State s, Blackhole bh) {
198+
s.consumer.bh = bh;
199+
s.table.forEach(s.consumer);
200+
}
201+
202+
@Benchmark
203+
public void iterate_hashMap(D2State s, Blackhole bh) {
204+
for (Map.Entry<Key2, Long> entry : s.hashMap.entrySet()) {
205+
bh.consume(entry.getKey());
206+
bh.consume(entry.getValue());
207+
}
208+
}
209+
}

internal-api/src/main/java/datadog/trace/util/HashingUtils.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ public static final int hash(int hash0, int hash1, int hash2, int hash3) {
7979
}
8080

8181
public static final int hash(Object obj0, Object obj1, Object obj2, Object obj3, Object obj4) {
82-
return hash(hashCode(obj0), hashCode(obj1), hashCode(obj2), hashCode(obj3));
82+
return hash(hashCode(obj0), hashCode(obj1), hashCode(obj2), hashCode(obj3), hashCode(obj4));
8383
}
8484

8585
public static final int hash(int hash0, int hash1, int hash2, int hash3, int hash4) {

0 commit comments

Comments
 (0)