Skip to content

Commit 5a70276

Browse files
optimize SyncMap (#12)
* improve(syncmap): replace array in ValueTransaction with fields * improve(syncmap): lock on the node instead of node.lock * improve(syncmap): simplify getMutableValue * improve(syncmap): extract some methods from SyncMap into Atomics and Sentinel * improve(syncmap): swap out flags for lambdas in Atomics * improve(syncmap): reduce aggressive fencing by going back to compareAndSet * improve(syncmap): restructure put method to shortcircuit on the fast path * improve(syncmap): further restructuring of the put method * improve(syncmap): rework node access and bulk operation locking * improve(syncmap): replace getAndSet with compareAndExchange * improve(syncmap): clean up logic for the stamp lock * improve(syncmap): clean up * chore(syncmap): fix javadoc for the SyncMap class * chore(syncmap): clean up minor style errors
1 parent a6d633a commit 5a70276

File tree

6 files changed

+1156
-1311
lines changed

6 files changed

+1156
-1311
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
.gradle
2+
.kotlin
23
build/
34
!gradle/wrapper/gradle-wrapper.jar
45
!**/src/main/**/build/

build-logic/src/main/kotlin/sync.common-conventions.gradle.kts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,18 @@ val libs = extensions.getByType(org.gradle.accessors.dm.LibrariesForLibs::class)
1414
plugins.withId("me.champeau.jmh") {
1515
extensions.configure(JmhParameters::class) {
1616
jmhVersion = libs.versions.jmh.get()
17+
18+
jvm.set(
19+
javaToolchains
20+
.launcherFor {
21+
languageVersion.set(JavaLanguageVersion.of(21))
22+
}
23+
.map { launcher ->
24+
launcher.executablePath
25+
.asFile
26+
.absolutePath
27+
}
28+
)
1729
}
1830

1931
tasks.named("compileJmhJava") {

collections/benchmarks.md

Lines changed: 78 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -4,53 +4,83 @@
44

55
The following benchmarks are a comparison of `SyncMap` vs `Collections#synchronizedMap()` vs `ConcurrentHashMap`.
66

7+
### Summary
8+
9+
The `SyncMap` has similar performance characteristics of the `ConcurrentHashMap`,
10+
with an advantage in write speed.
11+
12+
The following results were recorded on an _M4 Macbook Pro_, using 8 threads and
13+
100,000 entries for each benchmark:
14+
15+
- `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.
19+
20+
- `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.
24+
25+
- `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.
34+
35+
### Results
36+
737
```txt
8-
Benchmark (implementation) (mode) (size) Mode Cnt Score Error Units
9-
SyncMapBenchmark.getOnly ConcurrentHashMap none 100000 thrpt 5 28.412 ± 0.114 ops/ms
10-
SyncMapBenchmark.getOnly ConcurrentHashMap presize 100000 thrpt 5 28.349 ± 0.440 ops/ms
11-
SyncMapBenchmark.getOnly ConcurrentHashMap prepopulate 100000 thrpt 5 22.157 ± 0.216 ops/ms
12-
13-
SyncMapBenchmark.getOnly SyncMap none 100000 thrpt 5 26.269 ± 0.059 ops/ms
14-
SyncMapBenchmark.getOnly SyncMap presize 100000 thrpt 5 26.239 ± 0.112 ops/ms
15-
SyncMapBenchmark.getOnly SyncMap prepopulate 100000 thrpt 5 20.710 ± 0.596 ops/ms
16-
17-
SyncMapBenchmark.getOnly SynchronizedMap none 100000 thrpt 5 0.097 ± 0.117 ops/ms
18-
SyncMapBenchmark.getOnly SynchronizedMap presize 100000 thrpt 5 0.235 ± 0.603 ops/ms
19-
SyncMapBenchmark.getOnly SynchronizedMap prepopulate 100000 thrpt 5 0.290 ± 0.630 ops/ms
20-
21-
SyncMapBenchmark.putAndGet SyncMap none 100000 thrpt 5 3.134 ± 0.091 ops/ms
22-
SyncMapBenchmark.putAndGet SyncMap presize 100000 thrpt 5 3.123 ± 0.108 ops/ms
23-
SyncMapBenchmark.putAndGet SyncMap prepopulate 100000 thrpt 5 2.947 ± 0.053 ops/ms
24-
25-
SyncMapBenchmark.putAndGet ConcurrentHashMap none 100000 thrpt 5 1.269 ± 0.007 ops/ms
26-
SyncMapBenchmark.putAndGet ConcurrentHashMap presize 100000 thrpt 5 2.180 ± 0.141 ops/ms
27-
SyncMapBenchmark.putAndGet ConcurrentHashMap prepopulate 100000 thrpt 5 1.999 ± 0.264 ops/ms
28-
29-
SyncMapBenchmark.putAndGet SynchronizedMap none 100000 thrpt 5 0.085 ± 0.254 ops/ms
30-
SyncMapBenchmark.putAndGet SynchronizedMap presize 100000 thrpt 5 0.065 ± 0.169 ops/ms
31-
SyncMapBenchmark.putAndGet SynchronizedMap prepopulate 100000 thrpt 5 0.068 ± 0.123 ops/ms
32-
33-
SyncMapBenchmark.putOnly SyncMap none 100000 thrpt 5 1.682 ± 0.269 ops/ms
34-
SyncMapBenchmark.putOnly SyncMap presize 100000 thrpt 5 1.847 ± 0.738 ops/ms
35-
SyncMapBenchmark.putOnly SyncMap prepopulate 100000 thrpt 5 3.320 ± 0.115 ops/ms
36-
37-
SyncMapBenchmark.putOnly ConcurrentHashMap none 100000 thrpt 5 1.289 ± 0.050 ops/ms
38-
SyncMapBenchmark.putOnly ConcurrentHashMap presize 100000 thrpt 5 2.174 ± 0.359 ops/ms
39-
SyncMapBenchmark.putOnly ConcurrentHashMap prepopulate 100000 thrpt 5 2.342 ± 0.244 ops/ms
40-
41-
SyncMapBenchmark.putOnly SynchronizedMap none 100000 thrpt 5 0.154 ± 0.331 ops/ms
42-
SyncMapBenchmark.putOnly SynchronizedMap presize 100000 thrpt 5 0.126 ± 0.414 ops/ms
43-
SyncMapBenchmark.putOnly SynchronizedMap prepopulate 100000 thrpt 5 0.110 ± 0.382 ops/ms
44-
45-
SyncMapBenchmark.randomPutAndGet SyncMap none 100000 thrpt 5 1.484 ± 0.072 ops/ms
46-
SyncMapBenchmark.randomPutAndGet SyncMap presize 100000 thrpt 5 1.517 ± 0.012 ops/ms
47-
SyncMapBenchmark.randomPutAndGet SyncMap prepopulate 100000 thrpt 5 1.606 ± 0.007 ops/ms
48-
49-
SyncMapBenchmark.randomPutAndGet ConcurrentHashMap none 100000 thrpt 5 1.149 ± 0.010 ops/ms
50-
SyncMapBenchmark.randomPutAndGet ConcurrentHashMap presize 100000 thrpt 5 1.215 ± 0.034 ops/ms
51-
SyncMapBenchmark.randomPutAndGet ConcurrentHashMap prepopulate 100000 thrpt 5 1.188 ± 0.023 ops/ms
52-
53-
SyncMapBenchmark.randomPutAndGet SynchronizedMap none 100000 thrpt 5 0.130 ± 0.023 ops/ms
54-
SyncMapBenchmark.randomPutAndGet SynchronizedMap presize 100000 thrpt 5 0.137 ± 0.034 ops/ms
55-
SyncMapBenchmark.randomPutAndGet SynchronizedMap prepopulate 100000 thrpt 5 0.112 ± 0.033 ops/ms
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
5686
```

collections/readme.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ dependencies {
4646

4747
## Performance
4848

49-
The benchmarks below were recorded on a _M4 Mac Pro_.
49+
The benchmarks below were recorded on a _M4 Macbook Pro_.
5050

5151
- [SyncMap Benchmarks](benchmarks.md#syncmap)
5252

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

Lines changed: 20 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
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;
3839
import org.openjdk.jmh.annotations.OutputTimeUnit;
3940
import org.openjdk.jmh.annotations.Param;
4041
import org.openjdk.jmh.annotations.Scope;
@@ -45,19 +46,17 @@
4546
import org.openjdk.jmh.infra.Blackhole;
4647

4748
@BenchmarkMode(Mode.Throughput)
48-
@OutputTimeUnit(TimeUnit.MILLISECONDS)
49+
@OutputTimeUnit(TimeUnit.NANOSECONDS)
4950
@State(Scope.Benchmark)
5051
@Fork(1)
5152
@Warmup(iterations = 5)
5253
@Measurement(iterations = 5)
53-
@Threads(16)
5454
public class SyncMapBenchmark {
55+
private static final int SIZE = 100_000;
56+
5557
@Param({ "SyncMap", "SynchronizedMap", "ConcurrentHashMap" })
5658
private String implementation;
5759

58-
@Param("100000")
59-
private int size;
60-
6160
@Param({ "none", "presize", "prepopulate" })
6261
private String mode;
6362

@@ -69,57 +68,61 @@ public void setup() {
6968
final boolean prepopulate = "prepopulate".equalsIgnoreCase(this.mode);
7069

7170
if ("SyncMap".equalsIgnoreCase(this.implementation)) {
72-
this.map = presized ? new SyncMap<>(this.size) : new SyncMap<>();
71+
this.map = presized ? new SyncMap<>(SyncMapBenchmark.SIZE) : new SyncMap<>();
7372
} else if ("SynchronizedMap".equalsIgnoreCase(this.implementation)) {
7473
this.map = presized
75-
? Collections.synchronizedMap(new HashMap<>(this.size))
74+
? Collections.synchronizedMap(new HashMap<>(SyncMapBenchmark.SIZE))
7675
: Collections.synchronizedMap(new HashMap<>());
7776
} else if ("ConcurrentHashMap".equalsIgnoreCase(this.implementation)) {
78-
this.map = presized ? new ConcurrentHashMap<>(this.size) : new ConcurrentHashMap<>();
77+
this.map = presized ? new ConcurrentHashMap<>(SyncMapBenchmark.SIZE) : new ConcurrentHashMap<>();
7978
}
8079

8180
if(prepopulate) {
82-
for(int i = 0; i < this.size; i++) {
81+
for(int i = 0; i < SyncMapBenchmark.SIZE; i++) {
8382
this.map.put(i, i);
8483
}
84+
}
8585

86-
for(int i = 0; i < this.size; i++) {
87-
this.map.get(i);
88-
}
86+
if(presized && (this.map instanceof final SyncMap<Integer, Integer> sync)) {
87+
sync.promote();
8988
}
9089
}
9190

9291
@Benchmark
9392
@Threads(8)
93+
@OperationsPerInvocation(SyncMapBenchmark.SIZE)
9494
public void getOnly(final Blackhole blackhole) {
95-
for(int i = 0; i < this.size; i++) {
95+
for(int i = 0; i < SyncMapBenchmark.SIZE; i++) {
9696
blackhole.consume(this.map.get(i));
9797
}
9898
}
9999

100100
@Benchmark
101101
@Threads(8)
102+
@OperationsPerInvocation(SyncMapBenchmark.SIZE)
102103
public void putOnly(final Blackhole blackhole) {
103-
for(int i = 0; i < this.size; i++) {
104+
for(int i = 0; i < SyncMapBenchmark.SIZE; i++) {
104105
blackhole.consume(this.map.put(i, i));
105106
}
106107
}
107108

108109
@Benchmark
109110
@Threads(8)
111+
@OperationsPerInvocation(SyncMapBenchmark.SIZE)
110112
public void putAndGet(final Blackhole blackhole) {
111-
for(int i = 0; i < this.size; i++) {
113+
for(int i = 0; i < SyncMapBenchmark.SIZE; i++) {
112114
blackhole.consume(this.map.put(i, i));
113115
blackhole.consume(this.map.get(i));
114116
}
115117
}
116118

117119
@Benchmark
118120
@Threads(8)
121+
@OperationsPerInvocation(SyncMapBenchmark.SIZE)
119122
public void randomPutAndGet(final Blackhole blackhole) {
120123
final Random random = new Random(8);
121-
for(int i = 0; i < this.size; i++) {
122-
final int key = random.nextInt(this.size);
124+
for(int i = 0; i < SyncMapBenchmark.SIZE; i++) {
125+
final int key = random.nextInt(SyncMapBenchmark.SIZE);
123126
if(random.nextBoolean()) {
124127
blackhole.consume(this.map.put(key, key));
125128
} else {

0 commit comments

Comments
 (0)