diff --git a/proxy/src/main/java/com/velocitypowered/proxy/network/registry/packet/DensePacketRegistryMap.java b/proxy/src/main/java/com/velocitypowered/proxy/network/registry/packet/DensePacketRegistryMap.java index 0cd220631..26fb3a9b8 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/network/registry/packet/DensePacketRegistryMap.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/network/registry/packet/DensePacketRegistryMap.java @@ -17,86 +17,55 @@ package com.velocitypowered.proxy.network.registry.packet; -import static com.velocitypowered.proxy.util.MathUtil.nextHighestPowerOfTwo; - import com.velocitypowered.api.network.ProtocolVersion; import com.velocitypowered.proxy.network.ProtocolUtils; import com.velocitypowered.proxy.network.packet.Packet; import com.velocitypowered.proxy.network.packet.PacketReader; import com.velocitypowered.proxy.network.packet.PacketWriter; import com.velocitypowered.proxy.network.registry.packet.PacketRegistryBuilder.PacketMapping; +import com.velocitypowered.proxy.util.MathUtil; +import com.velocitypowered.proxy.util.hash.PerfectHash; import io.netty.buffer.ByteBuf; import it.unimi.dsi.fastutil.ints.Int2ObjectMap; import org.checkerframework.checker.nullness.qual.Nullable; /** - * Provides a packet registry map that is "dense", ideal for registries that are tightly packed - * together by ID. Lookups for readers are very fast (O(1)) and for writers uses an embedded - * open-addressing, probing hash map to conserve memory. + * Provides a packet registry map that is "dense". Lookups are very fast due to the use of + * perfect hashing. */ public class DensePacketRegistryMap implements PacketRegistryMap { private final PacketReader[] readersById; - private final PacketWriter[] writersByClass; - private final Class[] classesByKey; - private final int[] idsByKey; + private final PacketWriter[] writersBuckets; + private final Class[] classesBuckets; + private final int[] idsByClass; + private final int key; public DensePacketRegistryMap(Int2ObjectMap> mappings) { int size = mappings.keySet().stream().mapToInt(x -> x).max().orElse(0) + 1; - int hashTableSize = nextHighestPowerOfTwo(size); + int hashSize = MathUtil.nextHighestPowerOfTwo(size); + int[] classHashCodes = mappings.values().stream().map(m -> m.packetClass) + .mapToInt(Object::hashCode).toArray(); + + this.key = PerfectHash.findPerfectHashKey(classHashCodes, hashSize); this.readersById = new PacketReader[size]; - this.writersByClass = new PacketWriter[hashTableSize]; - this.classesByKey = new Class[hashTableSize]; - this.idsByKey = new int[hashTableSize]; + this.writersBuckets = new PacketWriter[hashSize]; + this.classesBuckets = new Class[hashSize]; + this.idsByClass = new int[hashSize]; for (PacketMapping value : mappings.values()) { + final int hashIdx = bucket(value.packetClass); + this.readersById[value.id] = value.reader; - this.place(value.id, value.packetClass, value.writer); + this.writersBuckets[hashIdx] = value.writer; + this.classesBuckets[hashIdx] = value.packetClass; + this.idsByClass[hashIdx] = value.id; } } - private void place(int packetId, Class key, PacketWriter value) { - int bucket = findEmpty(key); - this.writersByClass[bucket] = value; - this.classesByKey[bucket] = key; - this.idsByKey[bucket] = packetId; - } - - private int findEmpty(Class key) { - int start = key.hashCode() % this.classesByKey.length; - int index = start; - - for (;;) { - if (this.classesByKey[index] == null || this.classesByKey[index].equals(key)) { - // It's available, so no chance that this value exists anywhere in the map. - return index; - } - - if ((index = (index + 1) % this.classesByKey.length) == start) { - return -1; - } - } - } - - private int index(Class key) { - int start = key.hashCode() % this.classesByKey.length; - int index = start; - - for (;;) { - if (this.classesByKey[index] == null) { - // It's available, so no chance that this value exists anywhere in the map. - return -1; - } - if (key.equals(this.classesByKey[index])) { - return index; - } - - // Conflict, keep probing ... - if ((index = (index + 1) % this.classesByKey.length) == start) { - return -1; - } - } + private int bucket(final Object o) { + return PerfectHash.hash(o.hashCode(), this.key, this.classesBuckets.length); } @Override @@ -111,10 +80,10 @@ public class DensePacketRegistryMap implements PacketRegistryMap { @Override public

void writePacket(P packet, ByteBuf buf, ProtocolVersion version) { - int bucket = this.index(packet.getClass()); - if (bucket != -1) { - ProtocolUtils.writeVarInt(buf, this.idsByKey[bucket]); - this.writersByClass[bucket].write(buf, packet, version); + int bucket = this.bucket(packet.getClass()); + if (this.classesBuckets[bucket] == packet.getClass()) { + ProtocolUtils.writeVarInt(buf, this.idsByClass[bucket]); + this.writersBuckets[bucket].write(buf, packet, version); } else { throw new IllegalArgumentException(String.format( "Unable to find id for packet of type %s in protocol %s", @@ -126,9 +95,9 @@ public class DensePacketRegistryMap implements PacketRegistryMap { @Override public @Nullable Class lookupPacket(int id) { - for (int bucket = 0; bucket < this.idsByKey.length; bucket++) { - if (this.idsByKey[bucket] == id) { - return (Class) this.classesByKey[bucket]; + for (int bucket = 0; bucket < this.idsByClass.length; bucket++) { + if (this.idsByClass[bucket] == id) { + return (Class) this.classesBuckets[bucket]; } } return null; diff --git a/proxy/src/main/java/com/velocitypowered/proxy/util/MathUtil.java b/proxy/src/main/java/com/velocitypowered/proxy/util/MathUtil.java index 700bdf363..e58f88ea9 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/util/MathUtil.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/util/MathUtil.java @@ -26,8 +26,8 @@ public class MathUtil { * @return the next-highest power of 2 from {@code v} */ public static int nextHighestPowerOfTwo(int v) { - // https://graphics.stanford.edu/~seander/bithacks.html#RoundUpPowerOf2 - v--; + // https://graphics.stanford.edu/~seander/bithacks.html#RoundUpPowerOf2, except the initial + // decrement by v v |= v >> 1; v |= v >> 2; v |= v >> 4; diff --git a/proxy/src/main/java/com/velocitypowered/proxy/util/hash/PerfectHash.java b/proxy/src/main/java/com/velocitypowered/proxy/util/hash/PerfectHash.java new file mode 100644 index 000000000..7f7c9b4ec --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/util/hash/PerfectHash.java @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2018 Velocity Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.velocitypowered.proxy.util.hash; + +import it.unimi.dsi.fastutil.HashCommon; +import java.util.Arrays; +import java.util.concurrent.ThreadLocalRandom; + +/** + * Perfect hashing, used as an optimization for the {@code DensePacketRegistryMap}. + */ +public class PerfectHash { + + /** + * Determines a key that, when used with {@link #hash(Object, int, int)}, results in a perfect 1:1 + * representation of the hash codes given in the {@code hashCodes} when placed into a table of + * size {@code hashSize}. + * + * @param hashCodes the array of hash codes + * @param hashSize the size of the array to place the hash codes in + * @return the key for use with {@link #hash(Object, int, int)} + */ + public static int findPerfectHashKey(int[] hashCodes, int hashSize) { + int[] frequencies = new int[hashSize]; + int key = -1; + + do { + // need to clear the hashCodes and try again + Arrays.fill(frequencies, 0); + + ++key; + for (int elem : hashCodes) { + frequencies[hash(elem, key, hashSize)]++; + } + } while (!isPerfect(frequencies)); + + return key; + } + + private static boolean isPerfect(int[] frequencies) { + for (int frequency : frequencies) { + if (frequency != 0 && frequency != 1) { + return false; + } + } + return true; + } + + /** + * Determines the bucket that the key {@code o} belongs in a perfect hash table, with a key + * computed by {@link #findPerfectHashKey(int[], int)}. + * + * @param o the object to hash + * @param key the key computed by {@link #findPerfectHashKey(int[], int)} + * @param hashSize the size of the hash table + * @return the appropriate bucket + */ + public static int hash(Object o, int key, int hashSize) { + return Math.abs(HashCommon.mix(o.hashCode() + key)) % hashSize; + } +}