From 1761755d4dfc16cd020aee90c48761d98552531b Mon Sep 17 00:00:00 2001 From: Andrew Steinborn Date: Fri, 20 Aug 2021 03:59:34 -0400 Subject: [PATCH] Add perfect hashing for dense packet registries. Since we know the set of packet mappings is always fixed, we can avoid dealing with hash collisions by coming up with a perfect hash function for each registry. The algorithm used is very simple: we add an offset to the original object's hash code, mix the bits around (using fastutil HashCommon.mix()) and make sure the result is always positive before taking the remainder. The algorithm to generate the offset to the hash code (which we call a "key") is usually quick and is sped up by always rounding up to the next power of 2. With this, we speed up writing out packet data by completely eliminating any need to check for hash collisions. --- .../packet/DensePacketRegistryMap.java | 91 ++++++------------- .../velocitypowered/proxy/util/MathUtil.java | 4 +- .../proxy/util/hash/PerfectHash.java | 76 ++++++++++++++++ 3 files changed, 108 insertions(+), 63 deletions(-) create mode 100644 proxy/src/main/java/com/velocitypowered/proxy/util/hash/PerfectHash.java 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; + } +}