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;
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<PacketMapping<?>> 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 <P extends Packet> 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<? extends Packet> lookupPacket(int id) {
for (int bucket = 0; bucket < this.idsByKey.length; bucket++) {
if (this.idsByKey[bucket] == id) {
return (Class<? extends Packet>) this.classesByKey[bucket];
for (int bucket = 0; bucket < this.idsByClass.length; bucket++) {
if (this.idsByClass[bucket] == id) {
return (Class<? extends Packet>) this.classesBuckets[bucket];
}
}
return null;

Datei anzeigen

@ -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;

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;
}
}