3
0
Mirror von https://github.com/PaperMC/Velocity.git synchronisiert 2024-09-29 06:30:16 +02:00

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.
Dieser Commit ist enthalten in:
Andrew Steinborn 2021-08-20 03:59:34 -04:00
Ursprung 3bf9a47fbf
Commit 1761755d4d
3 geänderte Dateien mit 108 neuen und 63 gelöschten Zeilen

Datei anzeigen

@ -17,86 +17,55 @@
package com.velocitypowered.proxy.network.registry.packet; package com.velocitypowered.proxy.network.registry.packet;
import static com.velocitypowered.proxy.util.MathUtil.nextHighestPowerOfTwo;
import com.velocitypowered.api.network.ProtocolVersion; import com.velocitypowered.api.network.ProtocolVersion;
import com.velocitypowered.proxy.network.ProtocolUtils; import com.velocitypowered.proxy.network.ProtocolUtils;
import com.velocitypowered.proxy.network.packet.Packet; import com.velocitypowered.proxy.network.packet.Packet;
import com.velocitypowered.proxy.network.packet.PacketReader; import com.velocitypowered.proxy.network.packet.PacketReader;
import com.velocitypowered.proxy.network.packet.PacketWriter; import com.velocitypowered.proxy.network.packet.PacketWriter;
import com.velocitypowered.proxy.network.registry.packet.PacketRegistryBuilder.PacketMapping; 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 io.netty.buffer.ByteBuf;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap; import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.checker.nullness.qual.Nullable;
/** /**
* Provides a packet registry map that is "dense", ideal for registries that are tightly packed * Provides a packet registry map that is "dense". Lookups are very fast due to the use of
* together by ID. Lookups for readers are very fast (O(1)) and for writers uses an embedded * perfect hashing.
* open-addressing, probing hash map to conserve memory.
*/ */
public class DensePacketRegistryMap implements PacketRegistryMap { public class DensePacketRegistryMap implements PacketRegistryMap {
private final PacketReader<?>[] readersById; private final PacketReader<?>[] readersById;
private final PacketWriter[] writersByClass; private final PacketWriter[] writersBuckets;
private final Class<?>[] classesByKey; private final Class<?>[] classesBuckets;
private final int[] idsByKey; private final int[] idsByClass;
private final int key;
public DensePacketRegistryMap(Int2ObjectMap<PacketMapping<?>> mappings) { public DensePacketRegistryMap(Int2ObjectMap<PacketMapping<?>> mappings) {
int size = mappings.keySet().stream().mapToInt(x -> x).max().orElse(0) + 1; 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.readersById = new PacketReader[size];
this.writersByClass = new PacketWriter[hashTableSize]; this.writersBuckets = new PacketWriter[hashSize];
this.classesByKey = new Class[hashTableSize]; this.classesBuckets = new Class[hashSize];
this.idsByKey = new int[hashTableSize]; this.idsByClass = new int[hashSize];
for (PacketMapping<?> value : mappings.values()) { for (PacketMapping<?> value : mappings.values()) {
final int hashIdx = bucket(value.packetClass);
this.readersById[value.id] = value.reader; 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) { private int bucket(final Object o) {
int bucket = findEmpty(key); return PerfectHash.hash(o.hashCode(), this.key, this.classesBuckets.length);
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;
}
}
} }
@Override @Override
@ -111,10 +80,10 @@ public class DensePacketRegistryMap implements PacketRegistryMap {
@Override @Override
public <P extends Packet> void writePacket(P packet, ByteBuf buf, ProtocolVersion version) { public <P extends Packet> void writePacket(P packet, ByteBuf buf, ProtocolVersion version) {
int bucket = this.index(packet.getClass()); int bucket = this.bucket(packet.getClass());
if (bucket != -1) { if (this.classesBuckets[bucket] == packet.getClass()) {
ProtocolUtils.writeVarInt(buf, this.idsByKey[bucket]); ProtocolUtils.writeVarInt(buf, this.idsByClass[bucket]);
this.writersByClass[bucket].write(buf, packet, version); this.writersBuckets[bucket].write(buf, packet, version);
} else { } else {
throw new IllegalArgumentException(String.format( throw new IllegalArgumentException(String.format(
"Unable to find id for packet of type %s in protocol %s", "Unable to find id for packet of type %s in protocol %s",
@ -126,9 +95,9 @@ public class DensePacketRegistryMap implements PacketRegistryMap {
@Override @Override
public @Nullable Class<? extends Packet> lookupPacket(int id) { public @Nullable Class<? extends Packet> lookupPacket(int id) {
for (int bucket = 0; bucket < this.idsByKey.length; bucket++) { for (int bucket = 0; bucket < this.idsByClass.length; bucket++) {
if (this.idsByKey[bucket] == id) { if (this.idsByClass[bucket] == id) {
return (Class<? extends Packet>) this.classesByKey[bucket]; return (Class<? extends Packet>) this.classesBuckets[bucket];
} }
} }
return null; return null;

Datei anzeigen

@ -26,8 +26,8 @@ public class MathUtil {
* @return the next-highest power of 2 from {@code v} * @return the next-highest power of 2 from {@code v}
*/ */
public static int nextHighestPowerOfTwo(int v) { public static int nextHighestPowerOfTwo(int v) {
// https://graphics.stanford.edu/~seander/bithacks.html#RoundUpPowerOf2 // https://graphics.stanford.edu/~seander/bithacks.html#RoundUpPowerOf2, except the initial
v--; // decrement by v
v |= v >> 1; v |= v >> 1;
v |= v >> 2; v |= v >> 2;
v |= v >> 4; v |= v >> 4;

Datei anzeigen

@ -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 <https://www.gnu.org/licenses/>.
*/
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;
}
}