diff --git a/benchmarks/pom.xml b/benchmarks/pom.xml index e7fe5798c..ca214d83a 100644 --- a/benchmarks/pom.xml +++ b/benchmarks/pom.xml @@ -16,7 +16,8 @@ ${basedir}/.. - 1.33 + 1.34 + 1.12.7 benchmarks UTF-8 @@ -38,6 +39,11 @@ ${jmh.version} provided + + net.bytebuddy + byte-buddy + ${byte-buddy.version} + diff --git a/benchmarks/src/main/java/com/esotericsoftware/kryo/benchmarks/MapBenchmark.java b/benchmarks/src/main/java/com/esotericsoftware/kryo/benchmarks/MapBenchmark.java new file mode 100644 index 000000000..065de4535 --- /dev/null +++ b/benchmarks/src/main/java/com/esotericsoftware/kryo/benchmarks/MapBenchmark.java @@ -0,0 +1,290 @@ +/* Copyright (c) 2008-2018, Nathan Sweet + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following + * conditions are met: + * + * - Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided with the distribution. + * - Neither the name of Esoteric Software nor the names of its contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, + * BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT + * SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ + +package com.esotericsoftware.kryo.benchmarks; + +import com.esotericsoftware.kryo.util.CuckooObjectMap; +import com.esotericsoftware.kryo.util.IdentityMap; +import com.esotericsoftware.kryo.util.ObjectMap; + +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Random; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.infra.Blackhole; + +import net.bytebuddy.ByteBuddy; + +public class MapBenchmark { + + @Benchmark + public void read (ReadBenchmarkState state, Blackhole blackhole) { + state.read(blackhole); + } + + @Benchmark + public void miss (MissBenchmarkState state, Blackhole blackhole) { + state.miss(blackhole); + } + + @Benchmark + public void write (WriteBenchmarkState state, Blackhole blackhole) { + state.write(blackhole); + } + + @Benchmark + public void writeRead (WriteBenchmarkState state, Blackhole blackhole) { + state.readWrite(blackhole); + } + + @State(Scope.Thread) + public static class AbstractBenchmarkState { + @Param({"object", "identity", "cuckoo", "hash"}) public MapType mapType; + @Param({"integers", "strings", "classes"}) public DataSource dataSource; + @Param({"100", "500", "1000", "2500", "5000", "10000"}) public int numClasses; + @Param({"51"}) public int initialCapacity; + @Param({"0.7", "0.75", "0.8"}) public float loadFactor; + @Param({"8192"}) public int maxCapacity; + + MapAdapter map; + List data; + } + + @State(Scope.Thread) + public static class ReadBenchmarkState extends AbstractBenchmarkState { + + final Random random = new Random(123L); + + @Setup(Level.Trial) + public void setup () { + map = createMap(mapType, initialCapacity, loadFactor, maxCapacity); + data = dataSource.buildData(random, numClasses); + data.forEach(c -> map.put(c, 1)); + Collections.shuffle(data); + } + + public void read (Blackhole blackhole) { + data.stream() + .limit(numClasses) + .map(map::get) + .forEach(blackhole::consume); + } + } + + @State(Scope.Thread) + public static class MissBenchmarkState extends AbstractBenchmarkState { + + final Random random = new Random(123L); + + private List moreData; + + @Setup(Level.Trial) + public void setup () { + map = createMap(mapType, initialCapacity, loadFactor, maxCapacity); + data = dataSource.buildData(random, numClasses); + data.forEach(c -> map.put(c, 1)); + moreData = dataSource.buildData(random, numClasses); + } + + public void miss (Blackhole blackhole) { + moreData.stream() + .map(map::get) + .forEach(blackhole::consume); + } + } + + @State(Scope.Thread) + public static class WriteBenchmarkState extends AbstractBenchmarkState { + + final Random random = new Random(123L); + + @Setup(Level.Trial) + public void setup () { + map = createMap(mapType, initialCapacity, loadFactor, maxCapacity); + data = dataSource.buildData(random, numClasses); + Collections.shuffle(data); + } + + public void write (Blackhole blackhole) { + data.stream() + .map(c -> map.put(c, 1)) + .forEach(blackhole::consume); + } + + public void readWrite (Blackhole blackhole) { + data.forEach(c -> map.put(c, 1)); + Collections.shuffle(data); + + data.stream() + .limit(numClasses) + .map(map::get) + .forEach(blackhole::consume); + map.clear(); + } + } + + public enum MapType { + object, identity, cuckoo, hash + } + + public enum DataSource { + integers { + Object getData (Random random) { + return random.nextInt(); + } + }, + strings { + Object getData (Random random) { + int leftLimit = 97; // 'a' + int rightLimit = 122; // 'z' + int low = 10; + int high = 100; + int length = random.nextInt(high-low) + low; + return random.ints(leftLimit, rightLimit + 1) + .limit(length) + .collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append) + .toString(); + } + }, + classes { + Object getData (Random random) { + return new ByteBuddy() + .subclass(Object.class) + .make() + .load(MapBenchmark.class.getClassLoader()) + .getLoaded(); + } + }; + + abstract Object getData (Random random); + + public List buildData (Random random, int numClasses) { + return IntStream.rangeClosed(0, numClasses).mapToObj(i -> getData(random)) + .collect(Collectors.toList()); + } + } + + private static MapAdapter createMap(MapType mapType, int initialCapacity, float loadFactor, int maxCapacity) { + switch (mapType) { + case cuckoo: + return new CuckooMapAdapter<>(new CuckooObjectMap<>(initialCapacity, loadFactor), maxCapacity); + case object: + return new ObjectMapAdapter<>(new ObjectMap<>(initialCapacity, loadFactor), maxCapacity); + case identity: + return new ObjectMapAdapter<>(new IdentityMap<>(initialCapacity, loadFactor), maxCapacity); + case hash: + return new HashMapAdapter<>(new HashMap<>(initialCapacity, loadFactor)); + default: + throw new IllegalStateException("Unexpected value: " + mapType); + } + } + + interface MapAdapter { + V get (K key); + + V put (K key, V value); + + void clear (); + } + + static class ObjectMapAdapter implements MapAdapter { + private final ObjectMap delegate; + private final int maxCapacity; + + public ObjectMapAdapter (ObjectMap delegate, int maxCapacity) { + this.delegate = delegate; + this.maxCapacity = maxCapacity; + } + + @Override + public Integer get (K key) { + return delegate.get(key, -1); + } + + @Override + public Integer put (K key, Integer value) { + delegate.put(key, value); + return null; + } + + @Override + public void clear () { + delegate.clear(maxCapacity); + } + } + + static class CuckooMapAdapter implements MapAdapter { + private final CuckooObjectMap delegate; + private final int maxCapacity; + + public CuckooMapAdapter (CuckooObjectMap delegate, int maxCapacity) { + this.delegate = delegate; + this.maxCapacity = maxCapacity; + } + + @Override + public Integer get (K key) { + return delegate.get(key, -1); + } + + @Override + public Integer put (K key, Integer value) { + delegate.put(key, value); + return null; + } + + @Override + public void clear () { + delegate.clear(maxCapacity); + } + } + + private static class HashMapAdapter implements MapAdapter { + private final HashMap delegate; + + public HashMapAdapter (HashMap delegate) { + this.delegate = delegate; + } + + @Override + public Integer get (K key) { + return delegate.get(key); + } + + @Override + public Integer put (K key, Integer value) { + return delegate.put(key, value); + } + + @Override + public void clear () { + delegate.clear(); + } + } + +} diff --git a/src/com/esotericsoftware/kryo/util/IdentityMap.java b/src/com/esotericsoftware/kryo/util/IdentityMap.java index e39bd28b0..a42af1409 100644 --- a/src/com/esotericsoftware/kryo/util/IdentityMap.java +++ b/src/com/esotericsoftware/kryo/util/IdentityMap.java @@ -23,15 +23,13 @@ * when growing the table size. *

* This class performs fast contains and remove (typically O(1), worst case O(n) but that is rare in practice). Add may be - * slightly slower, depending on hash collisions. Hashcodes are rehashed to reduce collisions and the need to resize. Load factors - * greater than 0.91 greatly increase the chances to resize to the next higher POT size. + * slightly slower, depending on hash collisions. Load factors greater than 0.91 greatly increase the chances to resize to the + * next higher POT size. *

* Unordered sets and maps are not designed to provide especially fast iteration. *

- * This implementation uses linear probing with the backward shift algorithm for removal. Hashcodes are rehashed using Fibonacci - * hashing, instead of the more common power-of-two mask, to better distribute poor hashCodes (see Malte - * Skarupke's blog post). Linear probing continues to work even when all hashCodes collide, just more slowly. + * This implementation uses linear probing with the backward shift algorithm for removal. Linear probing continues to work even + * when all hashCodes collide, just more slowly. * @author Tommy Ettinger * @author Nathan Sweet */ public class IdentityMap extends ObjectMap { @@ -59,7 +57,23 @@ public IdentityMap (IdentityMap map) { } protected int place (K item) { - return (int)(System.identityHashCode(item) * 0x9E3779B97F4A7C15L >>> shift); + return System.identityHashCode(item) & mask; + } + + public V get (T key) { + for (int i = place(key);; i = i + 1 & mask) { + K other = keyTable[i]; + if (other == null) return null; + if (other == key) return valueTable[i]; + } + } + + public V get (K key, V defaultValue) { + for (int i = place(key);; i = i + 1 & mask) { + K other = keyTable[i]; + if (other == null) return defaultValue; + if (other == key) return valueTable[i]; + } } int locateKey (K key) { diff --git a/src/com/esotericsoftware/kryo/util/IdentityObjectIntMap.java b/src/com/esotericsoftware/kryo/util/IdentityObjectIntMap.java index b84cb7891..cef9b85a6 100644 --- a/src/com/esotericsoftware/kryo/util/IdentityObjectIntMap.java +++ b/src/com/esotericsoftware/kryo/util/IdentityObjectIntMap.java @@ -23,15 +23,13 @@ * allowed. No allocation is done except when growing the table size. *

* This class performs fast contains and remove (typically O(1), worst case O(n) but that is rare in practice). Add may be - * slightly slower, depending on hash collisions. Hashcodes are rehashed to reduce collisions and the need to resize. Load factors - * greater than 0.91 greatly increase the chances to resize to the next higher POT size. + * slightly slower, depending on hash collisions. Load factors greater than 0.91 greatly increase the chances to resize to the + * next higher POT size. *

* Unordered sets and maps are not designed to provide especially fast iteration. *

- * This implementation uses linear probing with the backward shift algorithm for removal. Hashcodes are rehashed using Fibonacci - * hashing, instead of the more common power-of-two mask, to better distribute poor hashCodes (see Malte - * Skarupke's blog post). Linear probing continues to work even when all hashCodes collide, just more slowly. + * This implementation uses linear probing with the backward shift algorithm for removal. Linear probing continues to work even + * when all hashCodes collide, just more slowly. * @author Nathan Sweet * @author Tommy Ettinger */ public class IdentityObjectIntMap extends ObjectIntMap { @@ -59,7 +57,15 @@ public IdentityObjectIntMap (IdentityObjectIntMap map) { } protected int place (K item) { - return (int)(System.identityHashCode(item) * 0x9E3779B97F4A7C15L >>> shift); + return System.identityHashCode(item) & mask; + } + + public int get (K key, int defaultValue) { + for (int i = place(key);; i = i + 1 & mask) { + K other = keyTable[i]; + if (other == null) return defaultValue; + if (other == key) return valueTable[i]; + } } int locateKey (K key) { diff --git a/src/com/esotericsoftware/kryo/util/IntMap.java b/src/com/esotericsoftware/kryo/util/IntMap.java index 438807181..df21676a3 100644 --- a/src/com/esotericsoftware/kryo/util/IntMap.java +++ b/src/com/esotericsoftware/kryo/util/IntMap.java @@ -187,14 +187,20 @@ private void putResize (int key, @Null V value) { public V get (int key) { if (key == 0) return hasZeroValue ? zeroValue : null; - int i = locateKey(key); - return i >= 0 ? valueTable[i] : null; + for (int i = place(key);; i = i + 1 & mask) { + int other = keyTable[i]; + if (other == 0) return null; + if (other == key) return valueTable[i]; + } } public V get (int key, @Null V defaultValue) { - if (key == 0) return hasZeroValue ? zeroValue : defaultValue; - int i = locateKey(key); - return i >= 0 ? valueTable[i] : defaultValue; + if (key == 0) return hasZeroValue ? zeroValue : null; + for (int i = place(key);; i = i + 1 & mask) { + int other = keyTable[i]; + if (other == 0) return defaultValue; + if (other == key) return valueTable[i]; + } } @Null diff --git a/src/com/esotericsoftware/kryo/util/ObjectIntMap.java b/src/com/esotericsoftware/kryo/util/ObjectIntMap.java index 9ce0a49fb..8b5331df0 100644 --- a/src/com/esotericsoftware/kryo/util/ObjectIntMap.java +++ b/src/com/esotericsoftware/kryo/util/ObjectIntMap.java @@ -65,7 +65,7 @@ public class ObjectIntMap implements Iterable> { protected int shift; /** A bitmask used to confine hashcodes to the size of the table. Must be all 1 bits in its low positions, ie a power of two - * minus 1. If {@link #place(Object)} is overriden, this can be used instead of {@link #shift} to isolate usable bits of a + * minus 1. If {@link #place(Object)} is overridden, this can be used instead of {@link #shift} to isolate usable bits of a * hash. */ protected int mask; diff --git a/src/com/esotericsoftware/kryo/util/ObjectMap.java b/src/com/esotericsoftware/kryo/util/ObjectMap.java index e63ef10bb..471e863bb 100644 --- a/src/com/esotericsoftware/kryo/util/ObjectMap.java +++ b/src/com/esotericsoftware/kryo/util/ObjectMap.java @@ -62,7 +62,7 @@ public class ObjectMap implements Iterable> { protected int shift; /** A bitmask used to confine hashcodes to the size of the table. Must be all 1 bits in its low positions, ie a power of two - * minus 1. If {@link #place(Object)} is overriden, this can be used instead of {@link #shift} to isolate usable bits of a + * minus 1. If {@link #place(Object)} is overridden, this can be used instead of {@link #shift} to isolate usable bits of a * hash. */ protected int mask; @@ -113,7 +113,7 @@ public ObjectMap (ObjectMap map) { * "https://probablydance.com/2018/06/16/fibonacci-hashing-the-optimization-that-the-world-forgot-or-a-better-alternative-to-integer-modulo/">Malte * Skarupke's blog post). *

- * This method can be overriden to customizing hashing. This may be useful eg in the unlikely event that most hashcodes are + * This method can be overridden to customizing hashing. This may be useful eg in the unlikely event that most hashcodes are * Fibonacci numbers, if keys provide poor or incorrect hashcodes, or to simplify hashing if keys provide high quality * hashcodes and don't need Fibonacci hashing: {@code return item.hashCode() & mask;} */ protected int place (K item) { @@ -121,7 +121,7 @@ protected int place (K item) { } /** Returns the index of the key if already present, else -(index + 1) for the next empty index. This can be overridden in this - * pacakge to compare for equality differently than {@link Object#equals(Object)}. */ + * package to compare for equality differently than {@link Object#equals(Object)}. */ int locateKey (K key) { if (key == null) throw new IllegalArgumentException("key cannot be null."); K[] keyTable = this.keyTable; @@ -174,14 +174,20 @@ private void putResize (K key, @Null V value) { /** Returns the value for the specified key, or null if the key is not in the map. */ @Null public V get (T key) { - int i = locateKey(key); - return i < 0 ? null : valueTable[i]; + for (int i = place(key);; i = i + 1 & mask) { + K other = keyTable[i]; + if (other == null) return null; + if (other.equals(key)) return valueTable[i]; + } } /** Returns the value for the specified key, or the default value if the key is not in the map. */ public V get (K key, @Null V defaultValue) { - int i = locateKey(key); - return i < 0 ? defaultValue : valueTable[i]; + for (int i = place(key);; i = i + 1 & mask) { + K other = keyTable[i]; + if (other == null) return defaultValue; + if (other.equals(key)) return valueTable[i]; + } } @Null