Skip to content

Commit b5d5443

Browse files
authored
Performance improvements for maps (#876)
* #845 Replace Fibonacci hashing with plain masking for identity maps and inline `get` methods * #845 Adjust JavaDoc * #845 Add updated `MapBenchmark`
1 parent 180729e commit b5d5443

File tree

7 files changed

+356
-28
lines changed

7 files changed

+356
-28
lines changed

benchmarks/pom.xml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@
1616

1717
<properties>
1818
<kryo.root>${basedir}/..</kryo.root>
19-
<jmh.version>1.33</jmh.version>
19+
<jmh.version>1.34</jmh.version>
20+
<byte-buddy.version>1.12.7</byte-buddy.version>
2021
<uberjar.name>benchmarks</uberjar.name>
2122
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
2223
</properties>
@@ -38,6 +39,11 @@
3839
<version>${jmh.version}</version>
3940
<scope>provided</scope>
4041
</dependency>
42+
<dependency>
43+
<groupId>net.bytebuddy</groupId>
44+
<artifactId>byte-buddy</artifactId>
45+
<version>${byte-buddy.version}</version>
46+
</dependency>
4147
</dependencies>
4248

4349
<build>
Lines changed: 290 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,290 @@
1+
/* Copyright (c) 2008-2018, Nathan Sweet
2+
* All rights reserved.
3+
*
4+
* Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following
5+
* conditions are met:
6+
*
7+
* - Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
8+
* - Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following
9+
* disclaimer in the documentation and/or other materials provided with the distribution.
10+
* - Neither the name of Esoteric Software nor the names of its contributors may be used to endorse or promote products derived
11+
* from this software without specific prior written permission.
12+
*
13+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING,
14+
* BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT
15+
* SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
16+
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
17+
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
18+
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
19+
20+
package com.esotericsoftware.kryo.benchmarks;
21+
22+
import com.esotericsoftware.kryo.util.CuckooObjectMap;
23+
import com.esotericsoftware.kryo.util.IdentityMap;
24+
import com.esotericsoftware.kryo.util.ObjectMap;
25+
26+
import java.util.Collections;
27+
import java.util.HashMap;
28+
import java.util.List;
29+
import java.util.Random;
30+
import java.util.stream.Collectors;
31+
import java.util.stream.IntStream;
32+
33+
import org.openjdk.jmh.annotations.Benchmark;
34+
import org.openjdk.jmh.annotations.Level;
35+
import org.openjdk.jmh.annotations.Param;
36+
import org.openjdk.jmh.annotations.Scope;
37+
import org.openjdk.jmh.annotations.Setup;
38+
import org.openjdk.jmh.annotations.State;
39+
import org.openjdk.jmh.infra.Blackhole;
40+
41+
import net.bytebuddy.ByteBuddy;
42+
43+
public class MapBenchmark {
44+
45+
@Benchmark
46+
public void read (ReadBenchmarkState state, Blackhole blackhole) {
47+
state.read(blackhole);
48+
}
49+
50+
@Benchmark
51+
public void miss (MissBenchmarkState state, Blackhole blackhole) {
52+
state.miss(blackhole);
53+
}
54+
55+
@Benchmark
56+
public void write (WriteBenchmarkState state, Blackhole blackhole) {
57+
state.write(blackhole);
58+
}
59+
60+
@Benchmark
61+
public void writeRead (WriteBenchmarkState state, Blackhole blackhole) {
62+
state.readWrite(blackhole);
63+
}
64+
65+
@State(Scope.Thread)
66+
public static class AbstractBenchmarkState {
67+
@Param({"object", "identity", "cuckoo", "hash"}) public MapType mapType;
68+
@Param({"integers", "strings", "classes"}) public DataSource dataSource;
69+
@Param({"100", "500", "1000", "2500", "5000", "10000"}) public int numClasses;
70+
@Param({"51"}) public int initialCapacity;
71+
@Param({"0.7", "0.75", "0.8"}) public float loadFactor;
72+
@Param({"8192"}) public int maxCapacity;
73+
74+
MapAdapter<Object, Integer> map;
75+
List<Object> data;
76+
}
77+
78+
@State(Scope.Thread)
79+
public static class ReadBenchmarkState extends AbstractBenchmarkState {
80+
81+
final Random random = new Random(123L);
82+
83+
@Setup(Level.Trial)
84+
public void setup () {
85+
map = createMap(mapType, initialCapacity, loadFactor, maxCapacity);
86+
data = dataSource.buildData(random, numClasses);
87+
data.forEach(c -> map.put(c, 1));
88+
Collections.shuffle(data);
89+
}
90+
91+
public void read (Blackhole blackhole) {
92+
data.stream()
93+
.limit(numClasses)
94+
.map(map::get)
95+
.forEach(blackhole::consume);
96+
}
97+
}
98+
99+
@State(Scope.Thread)
100+
public static class MissBenchmarkState extends AbstractBenchmarkState {
101+
102+
final Random random = new Random(123L);
103+
104+
private List<Object> moreData;
105+
106+
@Setup(Level.Trial)
107+
public void setup () {
108+
map = createMap(mapType, initialCapacity, loadFactor, maxCapacity);
109+
data = dataSource.buildData(random, numClasses);
110+
data.forEach(c -> map.put(c, 1));
111+
moreData = dataSource.buildData(random, numClasses);
112+
}
113+
114+
public void miss (Blackhole blackhole) {
115+
moreData.stream()
116+
.map(map::get)
117+
.forEach(blackhole::consume);
118+
}
119+
}
120+
121+
@State(Scope.Thread)
122+
public static class WriteBenchmarkState extends AbstractBenchmarkState {
123+
124+
final Random random = new Random(123L);
125+
126+
@Setup(Level.Trial)
127+
public void setup () {
128+
map = createMap(mapType, initialCapacity, loadFactor, maxCapacity);
129+
data = dataSource.buildData(random, numClasses);
130+
Collections.shuffle(data);
131+
}
132+
133+
public void write (Blackhole blackhole) {
134+
data.stream()
135+
.map(c -> map.put(c, 1))
136+
.forEach(blackhole::consume);
137+
}
138+
139+
public void readWrite (Blackhole blackhole) {
140+
data.forEach(c -> map.put(c, 1));
141+
Collections.shuffle(data);
142+
143+
data.stream()
144+
.limit(numClasses)
145+
.map(map::get)
146+
.forEach(blackhole::consume);
147+
map.clear();
148+
}
149+
}
150+
151+
public enum MapType {
152+
object, identity, cuckoo, hash
153+
}
154+
155+
public enum DataSource {
156+
integers {
157+
Object getData (Random random) {
158+
return random.nextInt();
159+
}
160+
},
161+
strings {
162+
Object getData (Random random) {
163+
int leftLimit = 97; // 'a'
164+
int rightLimit = 122; // 'z'
165+
int low = 10;
166+
int high = 100;
167+
int length = random.nextInt(high-low) + low;
168+
return random.ints(leftLimit, rightLimit + 1)
169+
.limit(length)
170+
.collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append)
171+
.toString();
172+
}
173+
},
174+
classes {
175+
Object getData (Random random) {
176+
return new ByteBuddy()
177+
.subclass(Object.class)
178+
.make()
179+
.load(MapBenchmark.class.getClassLoader())
180+
.getLoaded();
181+
}
182+
};
183+
184+
abstract Object getData (Random random);
185+
186+
public List<Object> buildData (Random random, int numClasses) {
187+
return IntStream.rangeClosed(0, numClasses).mapToObj(i -> getData(random))
188+
.collect(Collectors.toList());
189+
}
190+
}
191+
192+
private static MapAdapter<Object, Integer> createMap(MapType mapType, int initialCapacity, float loadFactor, int maxCapacity) {
193+
switch (mapType) {
194+
case cuckoo:
195+
return new CuckooMapAdapter<>(new CuckooObjectMap<>(initialCapacity, loadFactor), maxCapacity);
196+
case object:
197+
return new ObjectMapAdapter<>(new ObjectMap<>(initialCapacity, loadFactor), maxCapacity);
198+
case identity:
199+
return new ObjectMapAdapter<>(new IdentityMap<>(initialCapacity, loadFactor), maxCapacity);
200+
case hash:
201+
return new HashMapAdapter<>(new HashMap<>(initialCapacity, loadFactor));
202+
default:
203+
throw new IllegalStateException("Unexpected value: " + mapType);
204+
}
205+
}
206+
207+
interface MapAdapter<K, V> {
208+
V get (K key);
209+
210+
V put (K key, V value);
211+
212+
void clear ();
213+
}
214+
215+
static class ObjectMapAdapter<K> implements MapAdapter<K, Integer> {
216+
private final ObjectMap<K, Integer> delegate;
217+
private final int maxCapacity;
218+
219+
public ObjectMapAdapter (ObjectMap<K, Integer> delegate, int maxCapacity) {
220+
this.delegate = delegate;
221+
this.maxCapacity = maxCapacity;
222+
}
223+
224+
@Override
225+
public Integer get (K key) {
226+
return delegate.get(key, -1);
227+
}
228+
229+
@Override
230+
public Integer put (K key, Integer value) {
231+
delegate.put(key, value);
232+
return null;
233+
}
234+
235+
@Override
236+
public void clear () {
237+
delegate.clear(maxCapacity);
238+
}
239+
}
240+
241+
static class CuckooMapAdapter<K> implements MapAdapter<K, Integer> {
242+
private final CuckooObjectMap<K, Integer> delegate;
243+
private final int maxCapacity;
244+
245+
public CuckooMapAdapter (CuckooObjectMap<K, Integer> delegate, int maxCapacity) {
246+
this.delegate = delegate;
247+
this.maxCapacity = maxCapacity;
248+
}
249+
250+
@Override
251+
public Integer get (K key) {
252+
return delegate.get(key, -1);
253+
}
254+
255+
@Override
256+
public Integer put (K key, Integer value) {
257+
delegate.put(key, value);
258+
return null;
259+
}
260+
261+
@Override
262+
public void clear () {
263+
delegate.clear(maxCapacity);
264+
}
265+
}
266+
267+
private static class HashMapAdapter<K> implements MapAdapter<K, Integer> {
268+
private final HashMap<K, Integer> delegate;
269+
270+
public HashMapAdapter (HashMap<K, Integer> delegate) {
271+
this.delegate = delegate;
272+
}
273+
274+
@Override
275+
public Integer get (K key) {
276+
return delegate.get(key);
277+
}
278+
279+
@Override
280+
public Integer put (K key, Integer value) {
281+
return delegate.put(key, value);
282+
}
283+
284+
@Override
285+
public void clear () {
286+
delegate.clear();
287+
}
288+
}
289+
290+
}

src/com/esotericsoftware/kryo/util/IdentityMap.java

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,15 +23,13 @@
2323
* when growing the table size.
2424
* <p>
2525
* This class performs fast contains and remove (typically O(1), worst case O(n) but that is rare in practice). Add may be
26-
* slightly slower, depending on hash collisions. Hashcodes are rehashed to reduce collisions and the need to resize. Load factors
27-
* greater than 0.91 greatly increase the chances to resize to the next higher POT size.
26+
* slightly slower, depending on hash collisions. Load factors greater than 0.91 greatly increase the chances to resize to the
27+
* next higher POT size.
2828
* <p>
2929
* Unordered sets and maps are not designed to provide especially fast iteration.
3030
* <p>
31-
* This implementation uses linear probing with the backward shift algorithm for removal. Hashcodes are rehashed using Fibonacci
32-
* hashing, instead of the more common power-of-two mask, to better distribute poor hashCodes (see <a href=
33-
* "https://probablydance.com/2018/06/16/fibonacci-hashing-the-optimization-that-the-world-forgot-or-a-better-alternative-to-integer-modulo/">Malte
34-
* Skarupke's blog post</a>). Linear probing continues to work even when all hashCodes collide, just more slowly.
31+
* This implementation uses linear probing with the backward shift algorithm for removal. Linear probing continues to work even
32+
* when all hashCodes collide, just more slowly.
3533
* @author Tommy Ettinger
3634
* @author Nathan Sweet */
3735
public class IdentityMap<K, V> extends ObjectMap<K, V> {
@@ -59,7 +57,23 @@ public IdentityMap (IdentityMap<K, V> map) {
5957
}
6058

6159
protected int place (K item) {
62-
return (int)(System.identityHashCode(item) * 0x9E3779B97F4A7C15L >>> shift);
60+
return System.identityHashCode(item) & mask;
61+
}
62+
63+
public <T extends K> V get (T key) {
64+
for (int i = place(key);; i = i + 1 & mask) {
65+
K other = keyTable[i];
66+
if (other == null) return null;
67+
if (other == key) return valueTable[i];
68+
}
69+
}
70+
71+
public V get (K key, V defaultValue) {
72+
for (int i = place(key);; i = i + 1 & mask) {
73+
K other = keyTable[i];
74+
if (other == null) return defaultValue;
75+
if (other == key) return valueTable[i];
76+
}
6377
}
6478

6579
int locateKey (K key) {

src/com/esotericsoftware/kryo/util/IdentityObjectIntMap.java

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,15 +23,13 @@
2323
* allowed. No allocation is done except when growing the table size.
2424
* <p>
2525
* This class performs fast contains and remove (typically O(1), worst case O(n) but that is rare in practice). Add may be
26-
* slightly slower, depending on hash collisions. Hashcodes are rehashed to reduce collisions and the need to resize. Load factors
27-
* greater than 0.91 greatly increase the chances to resize to the next higher POT size.
26+
* slightly slower, depending on hash collisions. Load factors greater than 0.91 greatly increase the chances to resize to the
27+
* next higher POT size.
2828
* <p>
2929
* Unordered sets and maps are not designed to provide especially fast iteration.
3030
* <p>
31-
* This implementation uses linear probing with the backward shift algorithm for removal. Hashcodes are rehashed using Fibonacci
32-
* hashing, instead of the more common power-of-two mask, to better distribute poor hashCodes (see <a href=
33-
* "https://probablydance.com/2018/06/16/fibonacci-hashing-the-optimization-that-the-world-forgot-or-a-better-alternative-to-integer-modulo/">Malte
34-
* Skarupke's blog post</a>). Linear probing continues to work even when all hashCodes collide, just more slowly.
31+
* This implementation uses linear probing with the backward shift algorithm for removal. Linear probing continues to work even
32+
* when all hashCodes collide, just more slowly.
3533
* @author Nathan Sweet
3634
* @author Tommy Ettinger */
3735
public class IdentityObjectIntMap<K> extends ObjectIntMap<K> {
@@ -59,7 +57,15 @@ public IdentityObjectIntMap (IdentityObjectIntMap<K> map) {
5957
}
6058

6159
protected int place (K item) {
62-
return (int)(System.identityHashCode(item) * 0x9E3779B97F4A7C15L >>> shift);
60+
return System.identityHashCode(item) & mask;
61+
}
62+
63+
public int get (K key, int defaultValue) {
64+
for (int i = place(key);; i = i + 1 & mask) {
65+
K other = keyTable[i];
66+
if (other == null) return defaultValue;
67+
if (other == key) return valueTable[i];
68+
}
6369
}
6470

6571
int locateKey (K key) {

0 commit comments

Comments
 (0)