Files
Project-M/Packages/com.rukhanka.animation/Rukhanka.Toolbox/Perfect2HashTable.cs
T
2026-05-31 14:27:52 -07:00

228 lines
7.9 KiB
C#

using System.Runtime.CompilerServices;
using Unity.Assertions;
using Unity.Burst;
using Unity.Burst.CompilerServices;
using Unity.Collections;
using Unity.Collections.LowLevel.Unsafe;
using Unity.Mathematics;
using Random = Unity.Mathematics.Random;
/////////////////////////////////////////////////////////////////////////////////
namespace Rukhanka.Toolbox
{
// This is semi-perfect hash table tuned for query performance but with reasonable build time in mind
// True perfect hash table over known data can be made by brute forcing seed that produces hashes with all-unique
// positions for input data elements. But such brute forcing can be very lengthy and/or produce to big hash map
// data containers (array with many unoccupied slots). So, to reduce build time and space requirements, I've made
// a perfect hash table with a relaxed restriction: each slot can have maximum 1 collision (2 elements). With this
// constraint in mind table query can be very performant. It is also GPU friendly. I am suggesting very interesting
// perfect hash optimization research: https://www.youtube.com/watch?v=DMQ_HcNSOAI
[BurstCompile]
public static class Perfect2HashTable
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static unsafe int Query(uint v, uint seed, uint2* phtPtr, int phtLen)
{
var modMask = phtLen - 1;
var index = GetValueIndex(v, seed, modMask);
var phtv = phtPtr[index];
if (Hint.Likely(phtv.x == v))
return (int)(phtv.y & 0xffff);
var nextIndex = (int)(phtv.y >> 16);
var phtv2 = phtPtr[nextIndex];
if (Hint.Likely(phtv2.x == v))
return (int)(phtv2.y & 0xffff);
return -1;
}
/////////////////////////////////////////////////////////////////////////////////
[BurstCompile]
public static bool Build(in NativeArray<uint> inData, out NativeArray<uint2> outPht, out uint outSeed)
{
var numTries = 0xffff;
var maxPHTSize = 0xffff;
var phtSize = math.ceilpow2(inData.Length);
BurstAssert.IsTrue(phtSize <= maxPHTSize, $"Maximum table size is {maxPHTSize}. Requested size is {phtSize}");
var rng = new Random(0x811C9DC5);
while (phtSize <= maxPHTSize)
{
var phtSlotUsage = new NativeArray<int>(phtSize, Allocator.Temp, NativeArrayOptions.UninitializedMemory);
var seed = rng.NextUInt();
var i = 0;
for (; i < numTries; ++i)
{
if (BuildIteration(inData, ref phtSlotUsage, seed))
break;
seed = rng.NextUInt();
}
if (i < numTries)
{
outPht = MakePHTWithSeed(inData, seed, phtSlotUsage);
outSeed = seed;
return true;
}
phtSize <<= 1;
}
outPht = default;
outSeed = 0;
return false;
}
/////////////////////////////////////////////////////////////////////////////////
static NativeArray<uint2> MakePHTWithSeed(in NativeArray<uint> inData, uint seed, NativeArray<int> phtSlotUsage)
{
BurstAssert.IsTrue(math.ispow2(phtSlotUsage.Length), "Hash table size must be power of 2");
var modMask = phtSlotUsage.Length - 1;
var rv = new NativeArray<uint2>(phtSlotUsage.Length, Allocator.Temp);
for (var i = 0; i < inData.Length; ++i)
{
var v = inData[i];
var hv = Hash_Lemer(v, seed);
var index = (int)(hv & modMask);
var slotUsageCount = phtSlotUsage[index];
BurstAssert.IsTrue(slotUsageCount == 1 || slotUsageCount == 2, "Slot usage should be one or two");
// If slot already occupied, move value to next free slot
if (rv[index].x != 0)
{
for (int k = 0; k < rv.Length; ++k)
{
var nextIdx = (k + i) & modMask;
if (phtSlotUsage[nextIdx] == 0)
{
rv[nextIdx] = new uint2(v, (uint)i);
// Make a pointer to this slot from base slot
var iv = rv[index];
iv.y |= (uint)nextIdx << 16;
rv[index] = iv;
// Mark slot as occupied
phtSlotUsage[nextIdx] = 1;
break;
}
}
}
else
{
rv[index] = new uint2(v, (uint)i);
}
}
return rv;
}
/////////////////////////////////////////////////////////////////////////////////
static uint FNV1aBody(uint v, uint hash)
{
return (v ^ hash) * 0x01000193;
}
////////////////////////////////////////////////////////////////////////////////////
public static uint Hash_FNV1a(uint v, uint seed)
{
uint4 vb = new uint4(v, v >> 8, v >> 16, v >> 24);
seed = FNV1aBody(vb.x, seed);
seed = FNV1aBody(vb.y, seed);
seed = FNV1aBody(vb.z, seed);
seed = FNV1aBody(vb.w, seed);
return seed;
}
/////////////////////////////////////////////////////////////////////////////////
static uint Hash_CRC(uint v, uint seed)
{
const uint poly = 0x82f63b78;
for (var i = 0; i < 4; ++i)
{
seed ^= v >> (i * 8);
for (int k = 0; k < 8; k++)
seed = (seed & 1) != 0 ? (seed >> 1) ^ poly : seed >> 1;
}
return ~seed;
}
/////////////////////////////////////////////////////////////////////////////////
// High-Order Half of 64-Bit Product
// Ref "Hackers Delight" 8-2
static uint MultiplyHighUnsigned(uint u, uint v)
{
uint u0 = u & 0xffff;
uint u1 = u >> 16;
uint v0 = v & 0xffff;
uint v1 = v >> 16;
uint w0 = u0 * v0;
uint t = u1*v0 + (w0 >> 16);
uint w1 = t & 0xFFFF;
uint w2 = t >> 16;
w1 = u0 * v1 + w1;
return u1 * v1 + w2 + (w1 >> 16);
}
/////////////////////////////////////////////////////////////////////////////////
[MethodImpl(MethodImplOptions.AggressiveInlining)]
static uint Hash_Lemer_32bitAriphmetic(uint v, uint seed)
{
seed = v ^ seed;
uint tmpH = MultiplyHighUnsigned(seed, 0x4a39b70d);
uint tmpL = seed * 0x4a39b70d;
uint m1 = tmpH ^ tmpL;
tmpH = MultiplyHighUnsigned(m1, 0x12fad5c9);
tmpL = m1 * 0x12fad5c9;
var m2 = tmpH ^ tmpL;
return m2;
}
/////////////////////////////////////////////////////////////////////////////////
[MethodImpl(MethodImplOptions.AggressiveInlining)]
static uint Hash_Lemer(uint v, uint seed)
{
seed = v ^ seed;
ulong tmp = seed * 0x4a39b70dul;
var m1 = (uint)((tmp >> 32) ^ tmp);
tmp = m1 * 0x12fad5c9ul;
var m2 = (uint)((tmp >> 32) ^ tmp);
return m2;
}
/////////////////////////////////////////////////////////////////////////////////
[MethodImpl(MethodImplOptions.AggressiveInlining)]
static int GetValueIndex(uint v, uint seed, int modMask)
{
var hv = Hash_Lemer(v, seed);
var rv = (int)(hv & modMask);
return rv;
}
/////////////////////////////////////////////////////////////////////////////////
static unsafe bool BuildIteration(in NativeArray<uint> inData, ref NativeArray<int> phtSlotUsage, uint hashSeed)
{
UnsafeUtility.MemClear(phtSlotUsage.GetUnsafePtr(), phtSlotUsage.Length * sizeof(int));
var modMask = phtSlotUsage.Length - 1;
var maxCollisions = 0;
for (int i = 0; i < inData.Length; ++i)
{
var v = inData[i];
var index = GetValueIndex(v, hashSeed, modMask);
phtSlotUsage[index] += 1;
maxCollisions = math.max(phtSlotUsage[index], maxCollisions);
}
return maxCollisions <= 2;
}
}
}