Skip to content

Commit 23d20b4

Browse files
Optimize SyncMap and Update Benchmarks (#15)
* improve(syncmap): rework benchmarks, simplify read methods and move table initialization to first write * improve(syncmap): use weaker fencing for node search, and change replaceNode to use compareAndExchangeRelease * improve(syncmap): update benchmark results * chore(syncmap): clean up javadocs
1 parent dbf999a commit 23d20b4

File tree

3 files changed

+786
-507
lines changed

3 files changed

+786
-507
lines changed

collections/benchmarks.md

Lines changed: 72 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -13,74 +13,84 @@ The following results were recorded on an _M4 Macbook Pro_, using 8 threads and
1313
100,000 entries for each benchmark:
1414

1515
- `SyncMap` **get()** speed is...
16-
- 4.1% **SLOWER** than `ConcurrentHashMap` for an empty map.
17-
- 3.9% **SLOWER** than `ConcurrentHashMap` for a presized empty map.
18-
- 6.7% **SLOWER** than `ConcurrentHashMap` for a full map.
16+
- 47.8% **FASTER** than `ConcurrentHashMap` for an empty map.
17+
- 48.1% **FASTER** than `ConcurrentHashMap` for a presized empty map.
18+
- 21.0% **FASTER** than `ConcurrentHashMap` for a full map.
1919

2020
- `SyncMap` **put()** speed is...
21-
- 44.8% **FASTER** than `ConcurrentHashMap` for an empty map.
22-
- 30.0% **SLOWER** than `ConcurrentHashMap` for a presized empty map.
23-
- 53.9% **FASTER** than `ConcurrentHashMap` for a full map.
21+
- 23.8% **FASTER** than `ConcurrentHashMap` for an empty map.
22+
- 14.5% **SLOWER** than `ConcurrentHashMap` for a presized empty map.
23+
- 32.0% **FASTER** than `ConcurrentHashMap` for a full map.
2424

2525
- `SyncMap` **put()** then **get()** speed is...
26-
- 135.6% **FASTER** than `ConcurrentHashMap` for an empty map.
27-
- 66.1% **FASTER** than `ConcurrentHashMap` for a presized empty map.
28-
- 86.0% **FASTER** than `ConcurrentHashMap` for a full map.
29-
30-
- `SyncMap` random **put()** and **get()** speed is...
31-
- 39.1% **FASTER** than `ConcurrentHashMap` for an empty map.
32-
- 38.1% **FASTER** than `ConcurrentHashMap` for a presized empty map.
33-
- 37.0% **FASTER** than `ConcurrentHashMap` for a full map.
26+
- 109.7% **FASTER** than `ConcurrentHashMap` for an empty map.
27+
- 70.4% **FASTER** than `ConcurrentHashMap` for a presized empty map.
28+
- 55.1% **FASTER** than `ConcurrentHashMap` for a full map.
3429

3530
### Results
3631

3732
```txt
38-
Benchmark (implementation) (mode) Mode Cnt Score Error Units
39-
SyncMapBenchmark.getOnly ConcurrentHashMap none thrpt 5 2.954 ± 0.016 ops/ns
40-
SyncMapBenchmark.getOnly ConcurrentHashMap presize thrpt 5 2.936 ± 0.005 ops/ns
41-
SyncMapBenchmark.getOnly ConcurrentHashMap prepopulate thrpt 5 2.658 ± 0.011 ops/ns
42-
43-
SyncMapBenchmark.getOnly SyncMap none thrpt 5 2.833 ± 0.018 ops/ns
44-
SyncMapBenchmark.getOnly SyncMap presize thrpt 5 2.821 ± 0.004 ops/ns
45-
SyncMapBenchmark.getOnly SyncMap prepopulate thrpt 5 2.479 ± 0.021 ops/ns
46-
47-
SyncMapBenchmark.getOnly SynchronizedMap none thrpt 5 0.033 ± 0.045 ops/ns
48-
SyncMapBenchmark.getOnly SynchronizedMap presize thrpt 5 0.027 ± 0.004 ops/ns
49-
SyncMapBenchmark.getOnly SynchronizedMap prepopulate thrpt 5 0.035 ± 0.045 ops/ns
50-
51-
SyncMapBenchmark.putAndGet SyncMap none thrpt 5 0.391 ± 0.066 ops/ns
52-
SyncMapBenchmark.putAndGet SyncMap presize thrpt 5 0.427 ± 0.039 ops/ns
53-
SyncMapBenchmark.putAndGet SyncMap prepopulate thrpt 5 0.424 ± 0.020 ops/ns
54-
55-
SyncMapBenchmark.putAndGet ConcurrentHashMap none thrpt 5 0.166 ± 0.113 ops/ns
56-
SyncMapBenchmark.putAndGet ConcurrentHashMap presize thrpt 5 0.257 ± 0.030 ops/ns
57-
SyncMapBenchmark.putAndGet ConcurrentHashMap prepopulate thrpt 5 0.228 ± 0.064 ops/ns
58-
59-
SyncMapBenchmark.putAndGet SynchronizedMap none thrpt 5 0.012 ± 0.002 ops/ns
60-
SyncMapBenchmark.putAndGet SynchronizedMap presize thrpt 5 0.012 ± 0.003 ops/ns
61-
SyncMapBenchmark.putAndGet SynchronizedMap prepopulate thrpt 5 0.012 ± 0.007 ops/ns
62-
63-
SyncMapBenchmark.putOnly SyncMap none thrpt 5 0.239 ± 0.025 ops/ns
64-
SyncMapBenchmark.putOnly SyncMap presize thrpt 5 0.235 ± 0.044 ops/ns
65-
SyncMapBenchmark.putOnly SyncMap prepopulate thrpt 5 0.457 ± 0.026 ops/ns
66-
67-
SyncMapBenchmark.putOnly ConcurrentHashMap none thrpt 5 0.165 ± 0.008 ops/ns
68-
SyncMapBenchmark.putOnly ConcurrentHashMap presize thrpt 5 0.336 ± 0.010 ops/ns
69-
SyncMapBenchmark.putOnly ConcurrentHashMap prepopulate thrpt 5 0.297 ± 0.043 ops/ns
70-
71-
SyncMapBenchmark.putOnly SynchronizedMap none thrpt 5 0.023 ± 0.004 ops/ns
72-
SyncMapBenchmark.putOnly SynchronizedMap presize thrpt 5 0.023 ± 0.005 ops/ns
73-
SyncMapBenchmark.putOnly SynchronizedMap prepopulate thrpt 5 0.022 ± 0.003 ops/ns
74-
75-
SyncMapBenchmark.randomPutAndGet SyncMap none thrpt 5 0.192 ± 0.009 ops/ns
76-
SyncMapBenchmark.randomPutAndGet SyncMap presize thrpt 5 0.203 ± 0.007 ops/ns
77-
SyncMapBenchmark.randomPutAndGet SyncMap prepopulate thrpt 5 0.200 ± 0.002 ops/ns
78-
79-
SyncMapBenchmark.randomPutAndGet ConcurrentHashMap none thrpt 5 0.138 ± 0.002 ops/ns
80-
SyncMapBenchmark.randomPutAndGet ConcurrentHashMap presize thrpt 5 0.147 ± 0.001 ops/ns
81-
SyncMapBenchmark.randomPutAndGet ConcurrentHashMap prepopulate thrpt 5 0.146 ± 0.003 ops/ns
82-
83-
SyncMapBenchmark.randomPutAndGet SynchronizedMap none thrpt 5 0.017 ± 0.009 ops/ns
84-
SyncMapBenchmark.randomPutAndGet SynchronizedMap presize thrpt 5 0.016 ± 0.013 ops/ns
85-
SyncMapBenchmark.randomPutAndGet SynchronizedMap prepopulate thrpt 5 0.018 ± 0.004 ops/ns
33+
Benchmark (implementation) (mode) (prime) (readPercentage) (size) Mode Cnt Score Error Units
34+
SyncMapBenchmark.get_only SyncMap none false 50 100000 thrpt 5 2.411 ± 0.004 ops/ns
35+
SyncMapBenchmark.get_only SyncMap none true 50 100000 thrpt 5 2.405 ± 0.006 ops/ns
36+
SyncMapBenchmark.get_only SyncMap presize false 50 100000 thrpt 5 2.416 ± 0.006 ops/ns
37+
SyncMapBenchmark.get_only SyncMap presize true 50 100000 thrpt 5 2.398 ± 0.003 ops/ns
38+
SyncMapBenchmark.get_only SyncMap prepopulate false 50 100000 thrpt 5 1.928 ± 0.024 ops/ns
39+
SyncMapBenchmark.get_only SyncMap prepopulate true 50 100000 thrpt 5 1.948 ± 0.035 ops/ns
40+
41+
SyncMapBenchmark.get_only ConcurrentHashMap none false 50 100000 thrpt 5 1.631 ± 0.002 ops/ns
42+
SyncMapBenchmark.get_only ConcurrentHashMap none true 50 100000 thrpt 5 2.418 ± 0.002 ops/ns
43+
SyncMapBenchmark.get_only ConcurrentHashMap presize false 50 100000 thrpt 5 1.631 ± 0.004 ops/ns
44+
SyncMapBenchmark.get_only ConcurrentHashMap presize true 50 100000 thrpt 5 2.414 ± 0.001 ops/ns
45+
SyncMapBenchmark.get_only ConcurrentHashMap prepopulate false 50 100000 thrpt 5 1.593 ± 0.004 ops/ns
46+
SyncMapBenchmark.get_only ConcurrentHashMap prepopulate true 50 100000 thrpt 5 2.372 ± 0.049 ops/ns
47+
48+
SyncMapBenchmark.get_only SynchronizedMap none false 50 100000 thrpt 5 0.026 ± 0.005 ops/ns
49+
SyncMapBenchmark.get_only SynchronizedMap none true 50 100000 thrpt 5 0.027 ± 0.006 ops/ns
50+
SyncMapBenchmark.get_only SynchronizedMap presize false 50 100000 thrpt 5 0.034 ± 0.044 ops/ns
51+
SyncMapBenchmark.get_only SynchronizedMap presize true 50 100000 thrpt 5 0.031 ± 0.043 ops/ns
52+
SyncMapBenchmark.get_only SynchronizedMap prepopulate false 50 100000 thrpt 5 0.026 ± 0.007 ops/ns
53+
SyncMapBenchmark.get_only SynchronizedMap prepopulate true 50 100000 thrpt 5 0.026 ± 0.006 ops/ns
54+
55+
SyncMapBenchmark.get_put SyncMap none false 50 100000 thrpt 5 0.534 ± 0.053 ops/ns
56+
SyncMapBenchmark.get_put SyncMap none true 50 100000 thrpt 5 0.539 ± 0.064 ops/ns
57+
SyncMapBenchmark.get_put SyncMap presize false 50 100000 thrpt 5 0.532 ± 0.042 ops/ns
58+
SyncMapBenchmark.get_put SyncMap presize true 50 100000 thrpt 5 0.542 ± 0.041 ops/ns
59+
SyncMapBenchmark.get_put SyncMap prepopulate false 50 100000 thrpt 5 0.526 ± 0.047 ops/ns
60+
SyncMapBenchmark.get_put SyncMap prepopulate true 50 100000 thrpt 5 0.517 ± 0.025 ops/ns
61+
62+
SyncMapBenchmark.get_put ConcurrentHashMap none false 50 100000 thrpt 5 0.241 ± 0.002 ops/ns
63+
SyncMapBenchmark.get_put ConcurrentHashMap none true 50 100000 thrpt 5 0.257 ± 0.006 ops/ns
64+
SyncMapBenchmark.get_put ConcurrentHashMap presize false 50 100000 thrpt 5 0.329 ± 0.040 ops/ns
65+
SyncMapBenchmark.get_put ConcurrentHashMap presize true 50 100000 thrpt 5 0.318 ± 0.006 ops/ns
66+
SyncMapBenchmark.get_put ConcurrentHashMap prepopulate false 50 100000 thrpt 5 0.339 ± 0.095 ops/ns
67+
SyncMapBenchmark.get_put ConcurrentHashMap prepopulate true 50 100000 thrpt 5 0.349 ± 0.022 ops/ns
68+
69+
SyncMapBenchmark.get_put SynchronizedMap none false 50 100000 thrpt 5 0.020 ± 0.002 ops/ns
70+
SyncMapBenchmark.get_put SynchronizedMap none true 50 100000 thrpt 5 0.020 ± 0.009 ops/ns
71+
SyncMapBenchmark.get_put SynchronizedMap presize false 50 100000 thrpt 5 0.021 ± 0.003 ops/ns
72+
SyncMapBenchmark.get_put SynchronizedMap presize true 50 100000 thrpt 5 0.018 ± 0.013 ops/ns
73+
SyncMapBenchmark.get_put SynchronizedMap prepopulate false 50 100000 thrpt 5 0.019 ± 0.012 ops/ns
74+
SyncMapBenchmark.get_put SynchronizedMap prepopulate true 50 100000 thrpt 5 0.019 ± 0.014 ops/ns
75+
76+
SyncMapBenchmark.put_only SyncMap none false 50 100000 thrpt 5 0.255 ± 0.027 ops/ns
77+
SyncMapBenchmark.put_only SyncMap none true 50 100000 thrpt 5 0.242 ± 0.014 ops/ns
78+
SyncMapBenchmark.put_only SyncMap presize false 50 100000 thrpt 5 0.213 ± 0.017 ops/ns
79+
SyncMapBenchmark.put_only SyncMap presize true 50 100000 thrpt 5 0.265 ± 0.012 ops/ns
80+
SyncMapBenchmark.put_only SyncMap prepopulate false 50 100000 thrpt 5 0.239 ± 0.015 ops/ns
81+
SyncMapBenchmark.put_only SyncMap prepopulate true 50 100000 thrpt 5 0.408 ± 0.053 ops/ns
82+
83+
SyncMapBenchmark.put_only ConcurrentHashMap none false 50 100000 thrpt 5 0.206 ± 0.219 ops/ns
84+
SyncMapBenchmark.put_only ConcurrentHashMap none true 50 100000 thrpt 5 0.160 ± 0.025 ops/ns
85+
SyncMapBenchmark.put_only ConcurrentHashMap presize false 50 100000 thrpt 5 0.273 ± 0.029 ops/ns
86+
SyncMapBenchmark.put_only ConcurrentHashMap presize true 50 100000 thrpt 5 0.310 ± 0.047 ops/ns
87+
SyncMapBenchmark.put_only ConcurrentHashMap prepopulate false 50 100000 thrpt 5 0.313 ± 0.022 ops/ns
88+
SyncMapBenchmark.put_only ConcurrentHashMap prepopulate true 50 100000 thrpt 5 0.309 ± 0.027 ops/ns
89+
90+
SyncMapBenchmark.put_only SynchronizedMap none false 50 100000 thrpt 5 0.032 ± 0.040 ops/ns
91+
SyncMapBenchmark.put_only SynchronizedMap none true 50 100000 thrpt 5 0.023 ± 0.016 ops/ns
92+
SyncMapBenchmark.put_only SynchronizedMap presize false 50 100000 thrpt 5 0.025 ± 0.019 ops/ns
93+
SyncMapBenchmark.put_only SynchronizedMap presize true 50 100000 thrpt 5 0.026 ± 0.025 ops/ns
94+
SyncMapBenchmark.put_only SynchronizedMap prepopulate false 50 100000 thrpt 5 0.021 ± 0.001 ops/ns
95+
SyncMapBenchmark.put_only SynchronizedMap prepopulate true 50 100000 thrpt 5 0.022 ± 0.004 ops/ns
8696
```

collections/src/jmh/java/space/vectrix/sync/collections/SyncMapBenchmark.java

Lines changed: 79 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
import java.util.Collections;
2727
import java.util.HashMap;
2828
import java.util.Map;
29-
import java.util.Random;
29+
import java.util.SplittableRandom;
3030
import java.util.concurrent.ConcurrentHashMap;
3131
import java.util.concurrent.TimeUnit;
3232
import org.openjdk.jmh.annotations.Benchmark;
@@ -35,7 +35,6 @@
3535
import org.openjdk.jmh.annotations.Level;
3636
import org.openjdk.jmh.annotations.Measurement;
3737
import org.openjdk.jmh.annotations.Mode;
38-
import org.openjdk.jmh.annotations.OperationsPerInvocation;
3938
import org.openjdk.jmh.annotations.OutputTimeUnit;
4039
import org.openjdk.jmh.annotations.Param;
4140
import org.openjdk.jmh.annotations.Scope;
@@ -45,89 +44,112 @@
4544
import org.openjdk.jmh.annotations.Warmup;
4645
import org.openjdk.jmh.infra.Blackhole;
4746

48-
@BenchmarkMode(Mode.Throughput)
49-
@OutputTimeUnit(TimeUnit.NANOSECONDS)
50-
@State(Scope.Benchmark)
5147
@Fork(1)
5248
@Warmup(iterations = 5)
5349
@Measurement(iterations = 5)
50+
@BenchmarkMode(Mode.Throughput)
51+
@OutputTimeUnit(TimeUnit.NANOSECONDS)
5452
public class SyncMapBenchmark {
55-
private static final int SIZE = 100_000;
53+
@State(Scope.Benchmark)
54+
public static class Container {
55+
@Param({ "SyncMap", "ConcurrentHashMap", "SynchronizedMap" })
56+
private String implementation;
5657

57-
@Param({ "SyncMap", "SynchronizedMap", "ConcurrentHashMap" })
58-
private String implementation;
58+
@Param({ "none", "presize", "prepopulate" })
59+
private String mode;
5960

60-
@Param({ "none", "presize", "prepopulate" })
61-
private String mode;
61+
@Param({ "false", "true" })
62+
private boolean prime;
6263

63-
private Map<Integer, Integer> map;
64+
@Param({ "100000" })
65+
private int size;
6466

65-
@Setup(Level.Iteration)
66-
public void setup() {
67-
final boolean presized = !"none".equalsIgnoreCase(this.mode);
68-
final boolean prepopulate = "prepopulate".equalsIgnoreCase(this.mode);
67+
private Map<Integer, Integer> map;
6968

70-
if ("SyncMap".equalsIgnoreCase(this.implementation)) {
71-
this.map = presized ? new SyncMap<>(SyncMapBenchmark.SIZE) : new SyncMap<>();
72-
} else if ("SynchronizedMap".equalsIgnoreCase(this.implementation)) {
73-
this.map = presized
74-
? Collections.synchronizedMap(new HashMap<>(SyncMapBenchmark.SIZE))
75-
: Collections.synchronizedMap(new HashMap<>());
76-
} else if ("ConcurrentHashMap".equalsIgnoreCase(this.implementation)) {
77-
this.map = presized ? new ConcurrentHashMap<>(SyncMapBenchmark.SIZE) : new ConcurrentHashMap<>();
78-
}
69+
@Setup(Level.Iteration)
70+
public void setup() {
71+
final boolean presized = !"none".equalsIgnoreCase(this.mode);
72+
final boolean prepopulate = "prepopulate".equalsIgnoreCase(this.mode);
7973

80-
if(prepopulate) {
81-
for(int i = 0; i < SyncMapBenchmark.SIZE; i++) {
82-
this.map.put(i, i);
74+
switch(this.implementation) {
75+
case "SyncMap" -> this.map = presized ? new SyncMap<>(this.size) : new SyncMap<>();
76+
case "ConcurrentHashMap" -> this.map = presized ? new ConcurrentHashMap<>(this.size) : new ConcurrentHashMap<>();
77+
case "SynchronizedMap" -> this.map = presized
78+
? Collections.synchronizedMap(new HashMap<>(this.size))
79+
: Collections.synchronizedMap(new HashMap<>());
80+
default -> throw new IllegalArgumentException("Unable to identify implementation: " + this.implementation);
8381
}
84-
}
8582

86-
if(presized && (this.map instanceof final SyncMap<Integer, Integer> sync)) {
87-
sync.promote();
83+
if(prepopulate) {
84+
for(int i = 0; i < this.size; i++) {
85+
this.map.put(i, i);
86+
}
87+
}
88+
89+
if(this.prime) {
90+
if(this.map instanceof final SyncMap<Integer, Integer> syncMap) {
91+
syncMap.promote();
92+
} else {
93+
for(int i = 0; i < this.size; i++) {
94+
this.map.get(i);
95+
}
96+
}
97+
}
8898
}
8999
}
90100

91-
@Benchmark
92-
@Threads(8)
93-
@OperationsPerInvocation(SyncMapBenchmark.SIZE)
94-
public void getOnly(final Blackhole blackhole) {
95-
for(int i = 0; i < SyncMapBenchmark.SIZE; i++) {
96-
blackhole.consume(this.map.get(i));
101+
@State(Scope.Thread)
102+
public static class Sample {
103+
@Param({ "50" })
104+
private int readPercentage;
105+
106+
private int cursor;
107+
private int length;
108+
private boolean[] isRead;
109+
110+
@Setup(Level.Iteration)
111+
public void setup(final Container container) {
112+
this.length = container.size;
113+
this.isRead = new boolean[this.length];
114+
115+
final SplittableRandom random = new SplittableRandom(Thread.currentThread().getId());
116+
for(int i = 0; i < this.length; i++) {
117+
this.isRead[i] = random.nextInt(100) < this.readPercentage;
118+
}
119+
120+
this.cursor = 0;
121+
}
122+
123+
private int next() {
124+
final int i = this.cursor;
125+
this.cursor = (i + 1) % this.length;
126+
return i;
97127
}
98128
}
99129

100130
@Benchmark
101131
@Threads(8)
102-
@OperationsPerInvocation(SyncMapBenchmark.SIZE)
103-
public void putOnly(final Blackhole blackhole) {
104-
for(int i = 0; i < SyncMapBenchmark.SIZE; i++) {
105-
blackhole.consume(this.map.put(i, i));
106-
}
132+
public void get_only(final Container container, final Sample sample, final Blackhole blackhole) {
133+
final int key = sample.next();
134+
blackhole.consume(container.map.get(key));
107135
}
108136

109137
@Benchmark
110138
@Threads(8)
111-
@OperationsPerInvocation(SyncMapBenchmark.SIZE)
112-
public void putAndGet(final Blackhole blackhole) {
113-
for(int i = 0; i < SyncMapBenchmark.SIZE; i++) {
114-
blackhole.consume(this.map.put(i, i));
115-
blackhole.consume(this.map.get(i));
116-
}
139+
public void put_only(final Container container, final Sample sample, final Blackhole blackhole) {
140+
final int key = sample.next();
141+
blackhole.consume(container.map.put(key, key));
117142
}
118143

119144
@Benchmark
120145
@Threads(8)
121-
@OperationsPerInvocation(SyncMapBenchmark.SIZE)
122-
public void randomPutAndGet(final Blackhole blackhole) {
123-
final Random random = new Random(8);
124-
for(int i = 0; i < SyncMapBenchmark.SIZE; i++) {
125-
final int key = random.nextInt(SyncMapBenchmark.SIZE);
126-
if(random.nextBoolean()) {
127-
blackhole.consume(this.map.put(key, key));
128-
} else {
129-
blackhole.consume(this.map.get(key));
130-
}
146+
public void get_put(final Container container, final Sample sample, final Blackhole blackhole) {
147+
final int key = sample.next();
148+
149+
if(sample.isRead[key]) {
150+
blackhole.consume(container.map.get(key));
151+
} else {
152+
blackhole.consume(container.map.put(key, key));
131153
}
132154
}
133155
}

0 commit comments

Comments
 (0)