diff --git a/benchmarks/pom.xml b/benchmarks/pom.xml index 24b132ae2..39e3248f1 100644 --- a/benchmarks/pom.xml +++ b/benchmarks/pom.xml @@ -38,6 +38,13 @@ ${jmh.version} provided + + + net.bytebuddy + byte-buddy + 1.9.4 + + diff --git a/benchmarks/src/main/java/com/esotericsoftware/kryo/benchmarks/utils/FastGetIntMapBenchmark.java b/benchmarks/src/main/java/com/esotericsoftware/kryo/benchmarks/utils/FastGetIntMapBenchmark.java new file mode 100644 index 000000000..7b3d59a8c --- /dev/null +++ b/benchmarks/src/main/java/com/esotericsoftware/kryo/benchmarks/utils/FastGetIntMapBenchmark.java @@ -0,0 +1,198 @@ +package com.esotericsoftware.kryo.benchmarks.utils; + +import com.esotericsoftware.kryo.util.*; + +import java.util.*; +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; +import net.bytebuddy.implementation.FixedValue; +import net.bytebuddy.matcher.ElementMatchers; + +// mvn -f benchmarks/pom.xml compile exec:java -Dexec.args="-f 3 -wi 6 -i 3 -t 2 -w 2s -r 2 FastGetObjectMapBenchmark.read" +public class FastGetIntMapBenchmark { + + @Benchmark + public void read (ReadBenchmarkState state, Blackhole blackhole) { + state.read(blackhole); + } + + @Benchmark + public void write (BenchmarkState state, Blackhole blackhole) { + state.write(blackhole); + } + + @Benchmark + public void writeRead (BenchmarkState state, Blackhole blackhole) { + state.readWrite(blackhole); + } + + @State(Scope.Thread) + public static class AbstractBenchmarkState { + @Param({"500", "1000", "3000", "10000"}) public int numClasses; + @Param({"2048"}) public int maxCapacity; + @Param({"intMap", "fastGetIntMap"}) public MapType mapType; + + IntObjectMapAdapter map; + Object[] classes; + List integers; + } + + @State(Scope.Thread) + public static class BenchmarkState extends AbstractBenchmarkState { + + @Setup(Level.Trial) + public void setup () { + map = createMap(mapType, maxCapacity); + classes = IntStream.rangeClosed(0, numClasses).mapToObj(FastGetIntMapBenchmark::buildClass).toArray(); + integers = IntStream.rangeClosed(0, numClasses).boxed().collect(Collectors.toList()); + } + + public void write (Blackhole blackhole) { + integers.stream() + .map(id -> map.put(id, classes[id])) + .forEach(blackhole::consume); + } + + public void readWrite (Blackhole blackhole) { + // read + integers.stream() + .map(id -> map.put(id, classes[id])) + .forEach(blackhole::consume); + + final Random random = new Random(); + for (int i = 0; i < numClasses; i++) { + int key = random.nextInt(numClasses - 1); + blackhole.consume(map.get(key)); + } + + map.clear(); + } + } + + @State(Scope.Thread) + public static class ReadBenchmarkState extends AbstractBenchmarkState { + + @Setup(Level.Iteration) + public void setup () { + map = createMap(mapType, maxCapacity); + classes = IntStream.rangeClosed(0, numClasses).mapToObj(FastGetIntMapBenchmark::buildClass).toArray(); + integers = IntStream.rangeClosed(0, numClasses).boxed().collect(Collectors.toList()); + } + + @Setup(Level.Invocation) + public void shuffle(){ + Collections.shuffle(integers); + } + + public void read (Blackhole blackhole) { + integers.stream() + .limit(500) + .map(map::get) + .forEach(blackhole::consume); + } + } + + public enum MapType { + fastGetIntMap, intMap + } + + + + interface IntObjectMapAdapter { + V get (int key); + + V put (int key, V value); + + void clear(); + + } + + static class IntMapAdapter implements IntObjectMapAdapter { + private final IntMap delegate; + private final int maxCapacity; + + public IntMapAdapter (IntMap delegate, int maxCapacity) { + this.delegate = delegate; + this.maxCapacity = maxCapacity; + } + + @Override + public V get (int key) { + return delegate.get(key, null); + } + + @Override + public V put (int key, V value) { + delegate.put(key, value); + return null; + } + + @Override + public void clear() { + delegate.clear(maxCapacity); + } + } + + static class FstGetIntMapAdapter implements IntObjectMapAdapter { + private final FastGetIntMap delegate; + private final int maxCapacity; + + public FstGetIntMapAdapter (FastGetIntMap delegate, int maxCapacity) { + this.delegate = delegate; + this.maxCapacity = maxCapacity; + } + + @Override + public V get (int key) { + return delegate.get(key, null); + } + + @Override + public V put (int key, V value) { + delegate.put(key, value); + return null; + } + + @Override + public void clear() { + delegate.clear(maxCapacity); + } + } + + + private static IntObjectMapAdapter createMap (MapType mapType, int maxCapacity) { + switch (mapType) { + case intMap: + return new IntMapAdapter<>(new IntMap<>(), maxCapacity); + case fastGetIntMap: + return new FstGetIntMapAdapter<>(new FastGetIntMap<>(), maxCapacity); + default: + throw new IllegalStateException("Unexpected value: " + mapType); + } + } + + + private static Class buildClass (int i) { + return new ByteBuddy() + .subclass(Object.class) + .method(ElementMatchers.named("toString")) + .intercept(FixedValue.value(String.valueOf(i))) + .make() + .load(FastGetIntMapBenchmark.class.getClassLoader()) + .getLoaded(); + } + + + + +} \ No newline at end of file diff --git a/src/com/esotericsoftware/kryo/Kryo.java b/src/com/esotericsoftware/kryo/Kryo.java index 8c8264540..8e20325f9 100644 --- a/src/com/esotericsoftware/kryo/Kryo.java +++ b/src/com/esotericsoftware/kryo/Kryo.java @@ -287,7 +287,7 @@ public void addDefaultSerializer (Class type, SerializerFactory serializerFactor /** Instances with the specified class name will use the specified serializer when {@link #register(Class)} or * {@link #register(Class, int)} are called. * @see #setDefaultSerializer(Class) */ - private void addDefaultSerializer(String className, Class serializer) { + private void addDefaultSerializer (String className, Class serializer) { try { addDefaultSerializer(Class.forName(className), serializer); } catch (ClassNotFoundException e) { diff --git a/src/com/esotericsoftware/kryo/util/DefaultClassResolver.java b/src/com/esotericsoftware/kryo/util/DefaultClassResolver.java index f51755e32..d95a5c7db 100644 --- a/src/com/esotericsoftware/kryo/util/DefaultClassResolver.java +++ b/src/com/esotericsoftware/kryo/util/DefaultClassResolver.java @@ -38,7 +38,6 @@ public class DefaultClassResolver implements ClassResolver { protected final IntMap idToRegistration = new IntMap<>(); protected final CuckooObjectMap classToRegistration = new CuckooObjectMap<>(); - protected IdentityObjectIntMap classToNameId; protected IntMap nameIdToClass; protected ObjectMap nameToClass; @@ -165,7 +164,7 @@ public Registration readClass (Input input) { protected Registration readName (Input input) { int nameId = input.readVarInt(true); - if (nameIdToClass == null) nameIdToClass = new IntMap<>(); + if (nameIdToClass == null) nameIdToClass = new FastGetIntMap<>(); Class type = nameIdToClass.get(nameId); if (type == null) { // Only read the class name the first time encountered in object graph. diff --git a/src/com/esotericsoftware/kryo/util/FastGetIntMap.java b/src/com/esotericsoftware/kryo/util/FastGetIntMap.java new file mode 100644 index 000000000..09ae0cf8d --- /dev/null +++ b/src/com/esotericsoftware/kryo/util/FastGetIntMap.java @@ -0,0 +1,54 @@ +package com.esotericsoftware.kryo.util; + +public class FastGetIntMap extends IntMap { + /** Creates a new map with an initial capacity of 51 and a load factor of 0.8. */ + public FastGetIntMap () { + this(51, 0.8f); + } + + /** Creates a new map with a load factor of 0.8. + * @param initialCapacity If not a power of two, it is increased to the next nearest power of two. */ + public FastGetIntMap (int initialCapacity) { + this(initialCapacity, 0.8f); + } + + /** Creates a new map with a initial load factor of 0.8(which may be overloaded in resize funtion). + * @param initialCapacity If not a power of two, it is increased to the next nearest power of two. */ + public FastGetIntMap (int initialCapacity, float initialLoadFactor) { + super(initialCapacity, initialLoadFactor); + } + + /** Returns the value for the specified key, or null if the key is not in the map. unroll because of better performance + * (benchmark shows about 2% higher performance) */ + @Override + @Null + public V get (int key) { + if (key == 0) return hasZeroValue ? zeroValue : null; + int[] keyTable = this.keyTable; + for (int i = place(key);; i = i + 1 & mask) { + int other = keyTable[i]; + if (other == key) return valueTable[i]; // Same key was found. + if (other == 0) return null; // Empty space is available. + } + } + + @Override + /** Returns the value for the specified key, or the default value if the key is not in the map. unroll because of better + * performance */ + public V get (int key, @Null V defaultValue) { + if (key == 0) return hasZeroValue ? zeroValue : null; + int[] keyTable = this.keyTable; + for (int i = place(key);; i = i + 1 & mask) { + int other = keyTable[i]; + if (other == key) return valueTable[i]; // Same key was found. + if (other == 0) return defaultValue; // Empty space is available. + } + } + + /** 1. remove magic number so that minimize the computation cost 2. with low loadFactor, we don't need to consider the hash + * collision **/ + @Override + protected int place (int item) { + return item & mask; + } +} diff --git a/src/com/esotericsoftware/kryo/util/FastGetObjectMap.java b/src/com/esotericsoftware/kryo/util/FastGetObjectMap.java new file mode 100644 index 000000000..94f2b8b89 --- /dev/null +++ b/src/com/esotericsoftware/kryo/util/FastGetObjectMap.java @@ -0,0 +1,82 @@ +/* Copyright (c) 2008-2020, 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.util; + +/** this map extends from objectMap, optimized for better reading performance so there will be some tricky optimization **/ +public class FastGetObjectMap extends ObjectMap { + /** Creates a new map with an initial capacity of 51 and a load factor of 0.8. */ + public FastGetObjectMap () { + this(51, 0.8f); + } + + /** Creates a new map with a load factor of 0.8. + * @param initialCapacity If not a power of two, it is increased to the next nearest power of two. */ + public FastGetObjectMap (int initialCapacity) { + this(initialCapacity, 0.8f); + } + + /** Creates a new map with a initial load factor of 0.8(which may be overloaded in resize funtion). + * @param initialCapacity If not a power of two, it is increased to the next nearest power of two. */ + public FastGetObjectMap (int initialCapacity, float initialLoadFactor) { + super(initialCapacity, initialLoadFactor); + } + + /** Returns the value for the specified key, or null if the key is not in the map. unroll because of better performance + * (benchmark shows about 2% higher performance) */ + @Override + @Null + public V get (T key) { + K[] keyTable = this.keyTable; + for (int i = place(key);; i = i + 1 & mask) { + K other = keyTable[i]; + if (key.equals(other)) return valueTable[i]; // Same key was found. + if (other == null) return null; // Empty space is available. + } + } + + @Override + /** Returns the value for the specified key, or the default value if the key is not in the map. unroll because of better + * performance */ + public V get (K key, @Null V defaultValue) { + K[] keyTable = this.keyTable; + for (int i = place(key);; i = i + 1 & mask) { + K other = keyTable[i]; + if (key.equals(other)) return valueTable[i]; // Same key was found. + if (other == null) return defaultValue; // Empty space is available. + } + } + + /** 1. remove magic number so that minimize the computation cost 2. with low loadFactor, we don't need to consider the hash + * collision **/ + @Override + protected int place (K item) { + return item.hashCode() & mask; + } + + /* According to previous benchmark, different size have different best loadFactor **/ + @Override + protected float computeLoadFactor (int newSize) { + if (newSize <= 2048) { + return 0.7f; + } else { + return 0.5f; + } + } +} diff --git a/src/com/esotericsoftware/kryo/util/ObjectMap.java b/src/com/esotericsoftware/kryo/util/ObjectMap.java index 60310b16b..300d926cc 100644 --- a/src/com/esotericsoftware/kryo/util/ObjectMap.java +++ b/src/com/esotericsoftware/kryo/util/ObjectMap.java @@ -296,8 +296,14 @@ public void ensureCapacity (int additionalCapacity) { if (keyTable.length < tableSize) resize(tableSize); } + /** Subclass override to dynamically adjust loadFactor, get better performance. */ + float computeLoadFactor (int newSize) { + return loadFactor; + } + final void resize (int newSize) { int oldCapacity = keyTable.length; + loadFactor = computeLoadFactor(newSize); threshold = (int)(newSize * loadFactor); mask = newSize - 1; shift = Long.numberOfLeadingZeros(mask);