diff --git a/internal-api/src/jmh/java/datadog/trace/api/TagMapAccessBenchmark.java b/internal-api/src/jmh/java/datadog/trace/api/TagMapAccessBenchmark.java new file mode 100644 index 00000000000..de33c957e9a --- /dev/null +++ b/internal-api/src/jmh/java/datadog/trace/api/TagMapAccessBenchmark.java @@ -0,0 +1,168 @@ +package datadog.trace.api; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Threads; +import org.openjdk.jmh.annotations.Warmup; +import org.openjdk.jmh.infra.Blackhole; + +/** + * Throughput microbenchmark for the core {@link TagMap} access paths — insert (direct, via Ledger, + * and HashMap variants), raw-value read, and Entry read — over a representative HTTP-server-ish tag + * set. + * + *

Threading correctness. Runs at {@code @Threads(8)}. All shared state is + * immutable ({@link #NAMES}/{@link #VALUES}); every bit of mutable state lives in a + * {@code @State(Scope.Thread)} holder so threads never contend on a shared map, index, or reader + * flyweight. Earlier TagMap benchmarks shared a cross-thread counter/index, which turned the result + * into a contention measurement rather than a TagMap measurement — this layout avoids that. Indices + * are plain per-invocation locals. + * + *

Run configuration is baked into annotations rather than relying on {@code -Pjmh.*} flags + * (which the {@code me.champeau.jmh} plugin ignores). + * + *

Key findings (MacBook M1, 8 threads, Java 17): + * + *

+ * + * + * MacBook M1 with 8 threads (Java 17) + * + * Benchmark Mode Cnt Score Error Units + * TagMapAccessBenchmark.getEntry thrpt 5 95559437.524 ± 1381678.908 ops/s + * TagMapAccessBenchmark.getObject thrpt 5 95980166.452 ± 2217719.560 ops/s + * TagMapAccessBenchmark.insert thrpt 5 52523529.023 ± 1816998.150 ops/s + * TagMapAccessBenchmark.insert_hashMap thrpt 5 65344306.574 ± 4013136.530 ops/s + * TagMapAccessBenchmark.insert_hashMap_builderStyle thrpt 5 28057827.189 ± 1359655.664 ops/s + * TagMapAccessBenchmark.insert_via_ledger thrpt 5 41169656.095 ± 773264.754 ops/s + * + */ +@BenchmarkMode(Mode.Throughput) +@OutputTimeUnit(TimeUnit.SECONDS) +@Fork(2) +@Warmup(iterations = 3) +@Measurement(iterations = 5) +@Threads(8) +@State(Scope.Benchmark) +public class TagMapAccessBenchmark { + // a representative HTTP-server-ish tag set (immutable -> safe to share across threads) + static final String[] NAMES = { + "http.request.method", + "http.response.status_code", + "http.route", + "url.path", + "url.scheme", + "server.address", + "server.port", + "client.address", + "network.protocol.version", + "user_agent.original", + "span.kind", + "component", + "language", + "error", + "resource.name", + "service.name", + "operation.name", + "env", + }; + + static final Object[] VALUES = new Object[NAMES.length]; + + static { + for (int i = 0; i < NAMES.length; ++i) { + VALUES[i] = "value-" + i; + } + } + + /** + * Pre-populated read map, PER-THREAD ({@code Scope.Thread}): each thread owns its own map so + * reads don't contend on shared mutable state under {@code @Threads(8)}. + */ + @State(Scope.Thread) + public static class ReadMap { + TagMap map; + + @Setup(Level.Trial) + public void build() { + this.map = TagMap.create(); + for (int i = 0; i < NAMES.length; ++i) { + this.map.set(NAMES[i], VALUES[i]); + } + } + } + + @Benchmark + public TagMap insert() { + TagMap map = TagMap.create(); + for (int i = 0; i < NAMES.length; ++i) { + map.set(NAMES[i], VALUES[i]); + } + return map; + } + + @Benchmark + public TagMap insert_via_ledger() { + TagMap.Ledger ledger = TagMap.ledger(); + for (int i = 0; i < NAMES.length; ++i) { + ledger.set(NAMES[i], VALUES[i]); + } + return ledger.build(); + } + + @Benchmark + public Map insert_hashMap() { + HashMap map = new HashMap<>(); + for (int i = 0; i < NAMES.length; ++i) { + map.put(NAMES[i], VALUES[i]); + } + return map; + } + + /** + * Models the builder idiom for HashMap: accumulate into a staging map, then defensively copy. Two + * allocations, two fill passes — the honest cost of a HashMap-based builder pattern. + */ + @Benchmark + public Map insert_hashMap_builderStyle() { + HashMap staging = new HashMap<>(); + for (int i = 0; i < NAMES.length; ++i) { + staging.put(NAMES[i], VALUES[i]); + } + return new HashMap<>(staging); + } + + @Benchmark + public void getObject(ReadMap rm, Blackhole bh) { + for (int i = 0; i < NAMES.length; ++i) { + bh.consume(rm.map.getObject(NAMES[i])); + } + } + + @Benchmark + public void getEntry(ReadMap rm, Blackhole bh) { + for (int i = 0; i < NAMES.length; ++i) { + bh.consume(rm.map.getEntry(NAMES[i]).objectValue()); + } + } +} diff --git a/internal-api/src/jmh/java/datadog/trace/util/ImmutableMapBenchmark.java b/internal-api/src/jmh/java/datadog/trace/util/ImmutableMapBenchmark.java new file mode 100644 index 00000000000..f460a834185 --- /dev/null +++ b/internal-api/src/jmh/java/datadog/trace/util/ImmutableMapBenchmark.java @@ -0,0 +1,198 @@ +package datadog.trace.util; + +import datadog.trace.api.TagMap; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.TreeMap; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Threads; +import org.openjdk.jmh.annotations.Warmup; +import org.openjdk.jmh.infra.Blackhole; + +/** + * Read-side benchmark for precomputed, immutable / read-mostly maps that are shared across + * threads. Models the use case where a map is built once and then only read — often published and + * read concurrently by many threads. + * + *

Because nothing mutates after construction, a single shared instance ({@link Scope#Benchmark}) + * read by all {@code @Threads} is realistic and contention-free. This is the read-mostly + * counterpart to the per-thread mutable {@link SingleThreadedMapBenchmark} and the contended {@code + * ConcurrentHashtable} / {@code ThreadSafeMap} suites. + * + *

Compares {@code get} + {@code iterate} across {@link HashMap}, {@link LinkedHashMap}, {@link + * TreeMap}, {@link TagMap}, and {@link java.util.Map#copyOf} (via {@link + * CollectionUtils#tryMakeImmutableMap} — the JDK's compact, array-backed {@code + * ImmutableCollections.MapN}, which is what the agent actually uses for fixed config maps; Java + * 10+, falls back to the input map pre-10). {@code Map.copyOf}/{@code MapN} is the honest + * immutable-map baseline, not {@code HashMap}. + * + *

Lookups use {@code EQUAL_KEYS} (distinct String instances) to exercise {@code equals()}; + * {@code *_sameKey} variants reuse the original interned key instances to show the identity fast + * path — which is the common tracer case, since map keys are typically interned tag-name constants. + * (Results pending a fresh multi-JVM run — {@code Map.copyOf} only materializes the compact form on + * Java 10+.) + */ +@Fork(2) +@Warmup(iterations = 2) +@Measurement(iterations = 3) +@Threads(8) +@State(Scope.Benchmark) +public class ImmutableMapBenchmark { + static final String[] INSERTION_KEYS = { + "foo", "bar", "baz", "quux", "foobar", "foobaz", "key0", "key1", "key2", "key3" + }; + + // Distinct String instances (not the literals used to build the maps) so lookups exercise + // equals(), not identity -- the realistic case for keys arriving from parsing/decoding. + static final String[] EQUAL_KEYS = newEqualKeys(); + + static String[] newEqualKeys() { + String[] keys = new String[INSERTION_KEYS.length]; + for (int i = 0; i < INSERTION_KEYS.length; ++i) { + keys[i] = new String(INSERTION_KEYS[i]); + } + return keys; + } + + static void fill(Map map) { + for (int i = 0; i < INSERTION_KEYS.length; ++i) { + map.put(INSERTION_KEYS[i], i); + } + } + + // Built once, never mutated -- safe to share across the reader threads. + HashMap hashMap; + LinkedHashMap linkedHashMap; + TreeMap treeMap; + TagMap tagMap; + Map copyOfMap; + + @Setup(Level.Trial) + public void setUp() { + hashMap = new HashMap<>(); + fill(hashMap); + linkedHashMap = new LinkedHashMap<>(); + fill(linkedHashMap); + treeMap = new TreeMap<>(); + fill(treeMap); + tagMap = TagMap.create(); + for (int i = 0; i < INSERTION_KEYS.length; ++i) { + tagMap.set(INSERTION_KEYS[i], i); // primitive support + } + // JDK compact immutable map (MapN on Java 10+); the agent's actual fixed-map representation. + copyOfMap = CollectionUtils.tryMakeImmutableMap(hashMap); + } + + /** Per-thread lookup cursor so each reader thread cycles keys independently. */ + @State(Scope.Thread) + public static class Cursor { + int index = 0; + + String nextKey() { + return nextKey(EQUAL_KEYS); + } + + String nextKey(String[] keys) { + if (++index >= keys.length) index = 0; + return keys[index]; + } + } + + @Benchmark + public Integer get_hashMap(Cursor cursor) { + return hashMap.get(cursor.nextKey()); + } + + @Benchmark + public Integer get_hashMap_sameKey(Cursor cursor) { + return hashMap.get(cursor.nextKey(INSERTION_KEYS)); + } + + @Benchmark + public void iterate_hashMap(Blackhole blackhole) { + for (Map.Entry entry : hashMap.entrySet()) { + blackhole.consume(entry.getKey()); + blackhole.consume(entry.getValue()); + } + } + + @Benchmark + public Integer get_linkedHashMap(Cursor cursor) { + return linkedHashMap.get(cursor.nextKey()); + } + + @Benchmark + public void iterate_linkedHashMap(Blackhole blackhole) { + for (Map.Entry entry : linkedHashMap.entrySet()) { + blackhole.consume(entry.getKey()); + blackhole.consume(entry.getValue()); + } + } + + @Benchmark + public Integer get_treeMap(Cursor cursor) { + return treeMap.get(cursor.nextKey()); + } + + @Benchmark + public void iterate_treeMap(Blackhole blackhole) { + for (Map.Entry entry : treeMap.entrySet()) { + blackhole.consume(entry.getKey()); + blackhole.consume(entry.getValue()); + } + } + + @Benchmark + public int get_tagMap(Cursor cursor) { + return tagMap.getInt(cursor.nextKey()); + } + + @Benchmark + public int get_tagMap_sameKey(Cursor cursor) { + return tagMap.getInt(cursor.nextKey(INSERTION_KEYS)); + } + + @Benchmark + public void iterate_tagMap(Blackhole blackhole) { + for (TagMap.EntryReader entry : tagMap) { + blackhole.consume(entry.tag()); + blackhole.consume(entry.intValue()); + } + } + + @Benchmark + public void iterate_tagMap_forEach(Blackhole blackhole) { + // Taking advantage of passthrough of contextObj to avoid capturing lambda + tagMap.forEach( + blackhole, + (bh, entry) -> { + bh.consume(entry.tag()); + bh.consume(entry.intValue()); + }); + } + + @Benchmark + public Integer get_copyOf(Cursor cursor) { + return copyOfMap.get(cursor.nextKey()); + } + + @Benchmark + public Integer get_copyOf_sameKey(Cursor cursor) { + return copyOfMap.get(cursor.nextKey(INSERTION_KEYS)); + } + + @Benchmark + public void iterate_copyOf(Blackhole blackhole) { + for (Map.Entry entry : copyOfMap.entrySet()) { + blackhole.consume(entry.getKey()); + blackhole.consume(entry.getValue()); + } + } +} diff --git a/internal-api/src/jmh/java/datadog/trace/util/SingleThreadedMapBenchmark.java b/internal-api/src/jmh/java/datadog/trace/util/SingleThreadedMapBenchmark.java new file mode 100644 index 00000000000..d446ec4b7a7 --- /dev/null +++ b/internal-api/src/jmh/java/datadog/trace/util/SingleThreadedMapBenchmark.java @@ -0,0 +1,222 @@ +package datadog.trace.util; + +import datadog.trace.api.TagMap; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.TreeMap; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Threads; +import org.openjdk.jmh.annotations.Warmup; +import org.openjdk.jmh.infra.Blackhole; + +/** + * Benchmark for single-threaded (uncontended) map usage: each thread builds, mutates, reads, and + * discards its own maps. Models the common tracer pattern of assembling a short-lived map + * (e.g. span tags) on a single thread. + * + *

State is per-thread ({@link Scope#Thread}) so no map is ever shared — the read-mostly shared + * case lives in {@link ImmutableMapBenchmark}, and the contended case in the {@code + * ConcurrentHashtable} / {@code ThreadSafeMap} suites. Running at {@code @Threads(8)} keeps + * allocation / GC interactions visible without introducing lock contention. + * + *

Comparing different Map types: + * + *

    + *
  • (RECOMMENDED) HashMap — fastest general-purpose lookups + *
  • (RECOMMENDED) TagMap — preferred for storing tags; excels at primitives, copying, and + * builder idioms + *
  • TreeMap — when a custom Comparator is needed (see CaseInsensitiveMapBenchmark) + *
  • LinkedHashMap — only when insertion-order iteration is required; cost is paid at + * construction and in per-entry memory + *
+ * + *

Uncontended synchronization tax. A {@link Collections#synchronizedMap} case is included + * to measure what synchronization costs when there is no contention: because each thread + * owns its synchronized map, the monitor is only ever locked by one thread. On JVMs with biased + * locking (Java ≤ 11 by default) repeated same-thread locking should be nearly free; on Java 15+ + * (biased locking disabled by default, JEP 374) it pays the full uncontended CAS. The + * unsynchronized {@code hashMap} {@code get}/{@code iterate} methods are the in-harness baseline; + * the tax is the delta to the {@code synchronizedHashMap} equivalents. Comparing across JVM + * versions at stock flags shows the biased-locking effect. (Results pending a fresh multi-JVM run.) + */ +@Fork(2) +@Warmup(iterations = 2) +@Measurement(iterations = 3) +@Threads(8) +@State(Scope.Thread) +public class SingleThreadedMapBenchmark { + static final String[] INSERTION_KEYS = { + "foo", "bar", "baz", "quux", "foobar", "foobaz", "key0", "key1", "key2", "key3" + }; + + // Distinct String instances so lookups exercise equals(), not identity. + static final String[] EQUAL_KEYS = newEqualKeys(); + + static String[] newEqualKeys() { + String[] keys = new String[INSERTION_KEYS.length]; + for (int i = 0; i < INSERTION_KEYS.length; ++i) { + keys[i] = new String(INSERTION_KEYS[i]); + } + return keys; + } + + static void fill(Map map) { + for (int i = 0; i < INSERTION_KEYS.length; ++i) { + map.put(INSERTION_KEYS[i], i); + } + } + + static TagMap fillTagMap(TagMap map) { + for (int i = 0; i < INSERTION_KEYS.length; ++i) { + map.set(INSERTION_KEYS[i], i); // primitive support + } + return map; + } + + // Per-thread prebuilt maps for the read + clone benchmarks (built once per trial, per thread). + HashMap hashMap; + Map synchronizedHashMap; + TreeMap treeMap; + LinkedHashMap linkedHashMap; + TagMap tagMap; + int index = 0; + + @Setup(Level.Trial) + public void setUp() { + hashMap = new HashMap<>(); + fill(hashMap); + synchronizedHashMap = Collections.synchronizedMap(new HashMap<>(hashMap)); + treeMap = new TreeMap<>(); + fill(treeMap); + linkedHashMap = new LinkedHashMap<>(); + fill(linkedHashMap); + tagMap = fillTagMap(TagMap.create()); + } + + String nextLookupKey() { + if (++index >= EQUAL_KEYS.length) index = 0; + return EQUAL_KEYS[index]; + } + + // ---- construction: build cost + allocation ---- + + @Benchmark + public Map create_hashMap() { + HashMap map = new HashMap<>(); + fill(map); + return map; + } + + @Benchmark + public Map create_hashMap_sized() { + // Sizing is preferable for large maps, but in practice most of our maps fall within the + // default. + HashMap map = new HashMap<>(INSERTION_KEYS.length); + fill(map); + return map; + } + + @Benchmark + public Map create_synchronizedHashMap() { + Map map = Collections.synchronizedMap(new HashMap<>()); + fill(map); + return map; + } + + @Benchmark + public TreeMap create_treeMap() { + TreeMap map = new TreeMap<>(); + fill(map); + return map; + } + + @Benchmark + public LinkedHashMap create_linkedHashMap() { + LinkedHashMap map = new LinkedHashMap<>(); + fill(map); + return map; + } + + @Benchmark + public TagMap create_tagMap() { + return fillTagMap(TagMap.create()); + } + + @Benchmark + public TagMap create_tagMap_via_ledger() { + TagMap.Ledger ledger = TagMap.ledger(); + for (int i = 0; i < INSERTION_KEYS.length; ++i) { + ledger.set(INSERTION_KEYS[i], i); // primitive support + } + return ledger.build(); + } + + // ---- copy ---- + + @Benchmark + public Map clone_hashMap() { + return new HashMap<>(hashMap); + } + + @Benchmark + public Map clone_synchronizedHashMap() { + return Collections.synchronizedMap(new HashMap<>(hashMap)); + } + + @Benchmark + public TreeMap clone_treeMap() { + TreeMap map = new TreeMap<>(); + map.putAll(treeMap); + return map; + } + + @Benchmark + public LinkedHashMap clone_linkedHashMap() { + return new LinkedHashMap<>(linkedHashMap); + } + + @Benchmark + public TagMap clone_tagMap() { + return tagMap.copy(); + } + + // ---- read: unsynchronized baseline vs uncontended synchronized (biased-locking story) ---- + + @Benchmark + public Integer get_hashMap() { + return hashMap.get(nextLookupKey()); + } + + @Benchmark + public Integer get_synchronizedHashMap() { + return synchronizedHashMap.get(nextLookupKey()); + } + + @Benchmark + public void iterate_hashMap(Blackhole blackhole) { + for (Map.Entry entry : hashMap.entrySet()) { + blackhole.consume(entry.getKey()); + blackhole.consume(entry.getValue()); + } + } + + @Benchmark + public void iterate_synchronizedHashMap(Blackhole blackhole) { + // Collections.synchronizedMap requires the caller to synchronize during iteration; this is the + // correct usage and measures one (uncontended) monitor acquire around the traversal. + synchronized (synchronizedHashMap) { + for (Map.Entry entry : synchronizedHashMap.entrySet()) { + blackhole.consume(entry.getKey()); + blackhole.consume(entry.getValue()); + } + } + } +} diff --git a/internal-api/src/jmh/java/datadog/trace/util/UnsynchronizedMapBenchmark.java b/internal-api/src/jmh/java/datadog/trace/util/UnsynchronizedMapBenchmark.java deleted file mode 100644 index 42fec2b98e0..00000000000 --- a/internal-api/src/jmh/java/datadog/trace/util/UnsynchronizedMapBenchmark.java +++ /dev/null @@ -1,308 +0,0 @@ -package datadog.trace.util; - -import datadog.trace.api.TagMap; -import java.util.HashMap; -import java.util.LinkedHashMap; -import java.util.Map; -import java.util.TreeMap; -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; -import org.openjdk.jmh.infra.Blackhole; - -/** - * - * - *

    - * Benchmark comparing different Map-s... - *
  • (RECOMMENDED) HashMap - for fastest lookups - (not typically needed for tags) - *
  • (RECOMMENDED) TagMap - for storing tags - especially if copying between maps or using - * builders - *
  • TreeMap - better for custom Comparators - case-insensitive Maps (see - * CaseInsensitiveMapBenchmark) - *
  • LinkedHashMap - only when insertion order is needed - *
- * - *

TagMap is the preferred way to store tags. - * - *

TagMap excels at storing primitives, copying between TagMap instances, and builder idioms. - * - *

Iterator traversal with TagMap is relatively slow, but TagMap#forEach is on par (and slightly) - * faster than traditional map entry iteration. - * - *

HashMap & LinkedHashMap perform equally well on get operations. - * - *

HashMap is 2x faster throughput-wise to create and has less memory overhead because there's no - * linked list to capture insertion order. - * - *

TreeMap is useful when a custom Comparator is needed -- see CaseInsensitiveMapBenchmark - * - *

HashMap & TagMap also perform exceedingly well in cases where the exact same object is used - * for put & get operations. e.g. when using String literals or Class literals as keys - * MacBook M1 1 thread (Java 21) - * - * Benchmark Mode Cnt Score Error Units - * UnsynchronizedMapBenchmark.clone_hashMap thrpt 6 12482267.775 ± 236852.198 ops/s - * UnsynchronizedMapBenchmark.clone_linkedHashMap thrpt 6 12414187.888 ± 224418.265 ops/s - * UnsynchronizedMapBenchmark.clone_tagMap thrpt 6 49638156.234 ± 2972608.986 ops/s - * UnsynchronizedMapBenchmark.clone_treeMap thrpt 6 16201216.086 ± 619985.352 ops/s - * - * UnsynchronizedMapBenchmark.create_hashMap thrpt 6 22534042.260 ± 819970.046 ops/s - * UnsynchronizedMapBenchmark.create_hashMap_sized thrpt 6 21871270.375 ± 893842.109 ops/s - * UnsynchronizedMapBenchmark.create_linkedHashMap thrpt 6 12905731.242 ± 8930007.156 ops/s - * UnsynchronizedMapBenchmark.create_tagMap thrpt 6 15794277.380 ± 6069426.265 ops/s - * UnsynchronizedMapBenchmark.create_treeMap thrpt 6 4711961.814 ± 48582.934 ops/s - * - * UnsynchronizedMapBenchmark.get_hashMap thrpt 6 212201631.841 ± 6223069.782 ops/s - * UnsynchronizedMapBenchmark.get_hashMap_sameKey thrpt 6 392053406.085 ± 3938305.125 ops/s - * UnsynchronizedMapBenchmark.get_linkedHashMap thrpt 6 210734968.352 ± 3627805.282 ops/s - * UnsynchronizedMapBenchmark.get_tagMap thrpt 6 201864656.534 ± 4596147.771 ops/s - * UnsynchronizedMapBenchmark.get_tagMap_sameKey thrpt 6 256311645.716 ± 13315886.308 ops/s - * UnsynchronizedMapBenchmark.get_treeMap thrpt 6 94606404.423 ± 806879.890 ops/s - * - * MacBook M1 with 8 threads (Java 21) - * - * Benchmark Mode Cnt Score Error Units - * UnsynchronizedMapBenchmark.clone_hashMap thrpt 6 89645484.526 ± 6546683.185 ops/s - * UnsynchronizedMapBenchmark.clone_linkedHashMap thrpt 6 78233577.417 ± 7204526.742 ops/s - * UnsynchronizedMapBenchmark.clone_tagMap thrpt 6 315228772.058 ± 20689692.104 ops/s - * UnsynchronizedMapBenchmark.clone_treeMap thrpt 6 102416350.341 ± 7258040.561 ops/s - * - * UnsynchronizedMapBenchmark.create_hashMap thrpt 6 150462966.692 ± 11243713.572 ops/s - * UnsynchronizedMapBenchmark.create_hashMap_sized thrpt 6 111213025.138 ± 4593366.916 ops/s - * UnsynchronizedMapBenchmark.create_linkedHashMap thrpt 6 80882399.133 ± 19567359.487 ops/s - * UnsynchronizedMapBenchmark.create_tagMap thrpt 6 93026443.634 ± 11831456.794 ops/s - * UnsynchronizedMapBenchmark.create_tagMap_via_ledger thrpt 6 70769351.353 ± 3821543.185 ops/s - * UnsynchronizedMapBenchmark.create_treeMap thrpt 6 32737595.187 ± 2638992.844 ops/s - * - * UnsynchronizedMapBenchmark.get_hashMap thrpt 6 1154522356.093 ± 116525174.735 ops/s - * UnsynchronizedMapBenchmark.get_hashMap_sameKey thrpt 6 1760800709.734 ± 33551896.166 ops/s - * UnsynchronizedMapBenchmark.get_linkedHashMap thrpt 6 1191208257.933 ± 49810465.132 ops/s - * UnsynchronizedMapBenchmark.get_tagMap thrpt 6 933455574.646 ± 154146815.295 ops/s - * UnsynchronizedMapBenchmark.get_tagMap_sameKey thrpt 6 1138764608.359 ± 88352911.617 ops/s - * UnsynchronizedMapBenchmark.get_treeMap thrpt 6 490872723.682 ± 87017311.892 ops/s - * - * UnsynchronizedMapBenchmark.iterate_hashMap thrpt 6 351222668.708 ± 35242914.752 ops/s - * UnsynchronizedMapBenchmark.iterate_linkedHashMap thrpt 6 406635839.285 ± 55990655.235 ops/s - * UnsynchronizedMapBenchmark.iterate_tagMap thrpt 6 185264584.604 ± 15137886.028 ops/s - * UnsynchronizedMapBenchmark.iterate_tagMap_forEach thrpt 6 422407681.630 ± 19493455.109 ops/s - * UnsynchronizedMapBenchmark.iterate_treeMap thrpt 6 392884747.896 ± 80190674.417 ops/s - * - */ -@Fork(2) -@Warmup(iterations = 2) -@Measurement(iterations = 3) -@Threads(8) -public class UnsynchronizedMapBenchmark { - static final String[] INSERTION_KEYS = { - "foo", "bar", "baz", "quux", "foobar", "foobaz", "key0", "key1", "key2", "key3" - }; - - static final String[] EQUAL_KEYS = - init( - () -> { - String[] keys = new String[INSERTION_KEYS.length]; - for (int i = 0; i < INSERTION_KEYS.length; ++i) { - keys[i] = new String(INSERTION_KEYS[i]); - } - return keys; - }); - - static int sharedLookupIndex = 0; - - static String nextLookupKey() { - return nextLookupKey(EQUAL_KEYS); - } - - static String nextLookupKey(String[] keys) { - int localIndex = ++sharedLookupIndex; - if (localIndex >= keys.length) { - sharedLookupIndex = localIndex = 0; - } - return keys[localIndex]; - } - - static T init(Supplier supplier) { - return supplier.get(); - } - - static void fill(Map map) { - for (int i = 0; i < INSERTION_KEYS.length; ++i) { - map.put(INSERTION_KEYS[i], i); - } - } - - static HashMap _create_hashMap() { - HashMap map = new HashMap<>(); - fill(map); - return map; - } - - @Benchmark - public Map create_hashMap() { - return _create_hashMap(); - } - - @Benchmark - public Map create_hashMap_sized() { - return _create_hashMap_sized(); - } - - static final HashMap HASH_MAP = _create_hashMap(); - - @Benchmark - public Integer get_hashMap() { - return HASH_MAP.get(nextLookupKey()); - } - - @Benchmark - public void iterate_hashMap(Blackhole blackhole) { - for (Map.Entry entry : HASH_MAP.entrySet()) { - blackhole.consume(entry.getKey()); - blackhole.consume(entry.getValue()); - } - } - - @Benchmark - public Integer get_hashMap_sameKey() { - return HASH_MAP.get(nextLookupKey(INSERTION_KEYS)); - } - - @Benchmark - public Map clone_hashMap() { - return new HashMap<>(HASH_MAP); - } - - static Map _create_hashMap_sized() { - // Sizing is preferable for large maps, but in practice, most of our maps typically fall within - // the default - HashMap map = new HashMap<>(INSERTION_KEYS.length); - fill(map); - return map; - } - - static TreeMap _create_treeMap() { - TreeMap map = new TreeMap<>(); - fill(map); - return map; - } - - @Benchmark - public TreeMap create_treeMap() { - return _create_treeMap(); - } - - static final TreeMap TREE_MAP = _create_treeMap(); - - @Benchmark - public Integer get_treeMap() { - return TREE_MAP.get(nextLookupKey()); - } - - @Benchmark - public void iterate_treeMap(Blackhole blackhole) { - for (Map.Entry entry : TREE_MAP.entrySet()) { - blackhole.consume(entry.getKey()); - blackhole.consume(entry.getValue()); - } - } - - @Benchmark - public TreeMap clone_treeMap() { - TreeMap map = new TreeMap<>(); - map.putAll(TREE_MAP); - return map; - } - - static LinkedHashMap _create_linkedHashMap() { - LinkedHashMap map = new LinkedHashMap<>(); - fill(map); - return map; - } - - @Benchmark - public LinkedHashMap create_linkedHashMap() { - return _create_linkedHashMap(); - } - - static final LinkedHashMap LINKED_HASH_MAP = _create_linkedHashMap(); - - @Benchmark - public Integer get_linkedHashMap() { - return LINKED_HASH_MAP.get(nextLookupKey()); - } - - @Benchmark - public void iterate_linkedHashMap(Blackhole blackhole) { - for (Map.Entry entry : TREE_MAP.entrySet()) { - blackhole.consume(entry.getKey()); - blackhole.consume(entry.getValue()); - } - } - - @Benchmark - public LinkedHashMap clone_linkedHashMap() { - return new LinkedHashMap<>(LINKED_HASH_MAP); - } - - static TagMap _create_tagMap() { - TagMap map = TagMap.create(); - for (int i = 0; i < INSERTION_KEYS.length; ++i) { - map.set(INSERTION_KEYS[i], i); // taking advantage of primitive support - } - return map; - } - - @Benchmark - public TagMap create_tagMap() { - return _create_tagMap(); - } - - @Benchmark - public TagMap create_tagMap_via_ledger() { - TagMap.Ledger ledger = TagMap.ledger(); - for (int i = 0; i < INSERTION_KEYS.length; ++i) { - ledger.set(INSERTION_KEYS[i], i); // taking advantage of primitive support - } - return ledger.build(); - } - - static final TagMap TAG_MAP = _create_tagMap(); - - @Benchmark - public int get_tagMap() { - return TAG_MAP.getInt(nextLookupKey()); - } - - @Benchmark - public int get_tagMap_sameKey() { - return TAG_MAP.getInt(nextLookupKey(INSERTION_KEYS)); - } - - @Benchmark - public void iterate_tagMap(Blackhole blackhole) { - for (TagMap.EntryReader entry : TAG_MAP) { - blackhole.consume(entry.tag()); - blackhole.consume(entry.intValue()); - } - } - - @Benchmark - public void iterate_tagMap_forEach(Blackhole blackhole) { - // Taking advantage of passthrough of contextObj to avoid capturing lambda - TAG_MAP.forEach( - blackhole, - (bh, entry) -> { - bh.consume(entry.tag()); - bh.consume(entry.intValue()); - }); - } - - @Benchmark - public TagMap clone_tagMap() { - return TAG_MAP.copy(); - } -}