-
Notifications
You must be signed in to change notification settings - Fork 344
Expand file tree
/
Copy pathThreadSafeMapBenchmark.java
More file actions
187 lines (163 loc) · 6.88 KB
/
Copy pathThreadSafeMapBenchmark.java
File metadata and controls
187 lines (163 loc) · 6.88 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
package datadog.trace.util;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentSkipListMap;
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.Scope;
import org.openjdk.jmh.annotations.State;
import org.openjdk.jmh.annotations.Threads;
import org.openjdk.jmh.annotations.Warmup;
/**
*
*
* <ul>
* Benchmark comparing different approaches to filling and reading a Map in a multi-thread
* context.
* <li>ConcurrentMap - only when there are simultaneously readers & writers in multiple threads
* <li>HashMap via volatile - preferred for background thread updates
* <li>synchronized HashMap - when simultaneous readers & writers are uncommon (e.g. tags)
* </ul>
*
* <p>
*
* <p>In most situations in dd-java-agent, ConcurrentMaps are not necessarily needed and incur
* additional overhead. ConcurrentMaps make sense when concurrent writers are likely.
*
* <p>If a Map can be created atomically in one thread and then stored into a volatile, that is the
* preferred solution. For example, requesting an update from agent / API and then exposing to the
* rest of the tracer via a global.
*
* <p>If a Map needs to be written in a thread-safe manner, but is primarily accessed from one
* thread at a time, then a synchronized HashMap is usually the best option. <code>
* MacBook M1 with 1 thread (Java 21)
*
* Benchmark Mode Cnt Score Error Units
* ThreadSafeMapBenchmark.create_concHashMap thrpt 6 8081979.153 ± 261559.222 ops/s
* ThreadSafeMapBenchmark.create_concSkipListMap thrpt 6 2998832.124 ± 103708.038 ops/s
* ThreadSafeMapBenchmark.create_hashMap thrpt 6 24938311.610 ± 673725.902 ops/s
* ThreadSafeMapBenchmark.create_hashMap_synchronized thrpt 6 7971740.607 ± 121986.296 ops/s
*
* ThreadSafeMapBenchmark.get_concHashMap thrpt 6 173942565.340 ± 12003493.448 ops/s
* ThreadSafeMapBenchmark.get_concSkipListMap thrpt 6 79230298.061 ± 13007895.765 ops/s
* ThreadSafeMapBenchmark.get_hashMap_synchronized thrpt 6 98056657.832 ± 3413815.061 ops/s
* ThreadSafeMapBenchmark.get_hashMap_volatile thrpt 6 210511753.596 ± 5017502.317 ops/s
* </code> <code>
* MacBook M1 with 8 threads (Java 21)
*
* Benchmark Mode Cnt Score Error Units
* ThreadSafeMapBenchmark.create_concHashMap thrpt 6 58015351.219 ± 6201384.867 ops/s
* ThreadSafeMapBenchmark.create_concSkipListMap thrpt 6 19296105.790 ± 4516587.751 ops/s
* ThreadSafeMapBenchmark.create_hashMap thrpt 6 147917381.815 ± 22901897.589 ops/s
* ThreadSafeMapBenchmark.create_hashMap_synchronized thrpt 6 56466354.962 ± 13202034.783 ops/s
*
* ThreadSafeMapBenchmark.get_concHashMap thrpt 6 849986442.797 ± 14499355.893 ops/s
* ThreadSafeMapBenchmark.get_concSkipListMap thrpt 6 26828246.629 ± 2772377.532 ops/s
* ThreadSafeMapBenchmark.get_hashMap_synchronized thrpt 6 20123419.604 ± 4858466.787 ops/s
* ThreadSafeMapBenchmark.get_hashMap_volatile thrpt 6 286024211.995 ± 114449056.603 ops/s
* </code>
*/
@Fork(2)
@Warmup(iterations = 2)
@Measurement(iterations = 3)
@Threads(8)
@State(Scope.Thread)
public class ThreadSafeMapBenchmark {
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 <T> T init(Supplier<T> supplier) {
return supplier.get();
}
// Per-thread (@State(Scope.Thread)) so cycling the lookup key doesn't contend a shared counter.
// The maps below stay static/shared (the point — concurrent reads of one map); only the index is
// per-thread. A shared counter's cache-line ping-pong would otherwise floor the fastest reads
// (e.g. FlatHashtable's lock-free probe), hiding exactly the differences this benchmark compares.
int lookupIndex = 0;
String nextLookupKey() {
return nextLookupKey(EQUAL_KEYS);
}
String nextLookupKey(String[] keys) {
int localIndex = ++lookupIndex;
if (localIndex >= keys.length) {
lookupIndex = localIndex = 0;
}
return keys[localIndex];
}
static void fill(Map<String, Integer> map) {
for (int i = 0; i < INSERTION_KEYS.length; ++i) {
map.put(INSERTION_KEYS[i], i);
}
}
static final HashMap<String, Integer> _create_hashMap() {
HashMap<String, Integer> map = new HashMap<>();
fill(map);
return map;
}
@Benchmark
public Map<String, Integer> create_hashMap() {
return _create_hashMap();
}
static volatile HashMap<String, Integer> VOLATILE_HASH_MAP = _create_hashMap();
@Benchmark
public Integer get_hashMap_volatile() {
Map<String, Integer> map = VOLATILE_HASH_MAP;
return map.get(nextLookupKey());
}
static final Map<String, Integer> _create_hashMap_synchronized() {
Map<String, Integer> map = Collections.synchronizedMap(new HashMap<>());
fill(map);
return map;
}
@Benchmark
public Map<String, Integer> create_hashMap_synchronized() {
return _create_hashMap_synchronized();
}
static final Map<String, Integer> SYNC_HASH_MAP = _create_hashMap_synchronized();
@Benchmark
public Integer get_hashMap_synchronized() {
return SYNC_HASH_MAP.get(nextLookupKey());
}
static ConcurrentHashMap<String, Integer> _create_concHashMap() {
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
fill(map);
return map;
}
@Benchmark
public ConcurrentHashMap<String, Integer> create_concHashMap() {
return _create_concHashMap();
}
static final ConcurrentHashMap<String, Integer> CONC_HASH_MAP = _create_concHashMap();
@Benchmark
public Integer get_concHashMap() {
return CONC_HASH_MAP.get(nextLookupKey());
}
static ConcurrentSkipListMap<String, Integer> _create_concSkipListMap() {
ConcurrentSkipListMap<String, Integer> map = new ConcurrentSkipListMap<>();
fill(map);
return map;
}
@Benchmark
public ConcurrentSkipListMap<String, Integer> create_concSkipListMap() {
return _create_concSkipListMap();
}
static final ConcurrentSkipListMap<String, Integer> CONC_SKIP_LIST_MAP =
_create_concSkipListMap();
@Benchmark
public Integer get_concSkipListMap() {
return CONC_SKIP_LIST_MAP.get(nextLookupKey());
}
}