Coverage Summary for Class: PerfectHashFinder (com.ghost.serialization.compiler)
| Class |
Class, %
|
Method, %
|
Branch, %
|
Line, %
|
Instruction, %
|
| PerfectHashFinder |
100%
(1/1)
|
100%
(4/4)
|
88.5%
(46/52)
|
96.5%
(55/57)
|
96.3%
(420/436)
|
@file:Suppress("ReplaceSizeCheckWithIsNotEmpty")
package com.ghost.serialization.compiler
import com.ghost.serialization.compiler.GhostEmitterConstants as C
internal data class PerfectHashConfig(
val shift: Int,
val multiplier: Int,
val tableSize: Int,
val extendedKeyHash: Boolean
)
internal object PerfectHashFinder {
/**
* Utility to find a collision-free hash multiplier and shift configuration
* mapping a set of field names to unique index slots inside a 1024-entry table.
*
* ### How the Reader uses these parameters (Runtime Walkthrough):
*
* Suppose we have a field named **"age"** (3 bytes: 'a'=97, 'g'=103, 'e'=101)
* and the optimizer chose `multiplier = 31` and `shift = 5`.
*
* | Step | Action | Description | Resulting Value |
* | :--- | :--- | :--- | :--- |
* | **1. Bytes** | Capture | Read the first 4 bytes of "age" | `[97, 103, 101]` |
* | **2. Packing** | Create `key` | Place bytes into 4 slots (32-bit container) | `0x00656761` (hex) |
* | **3. Hash** | Apply formula | Calculate: `((key * mult + size) >> shift) & 1023` | `((6645601 * 31 + 3) >> 5) & 1023` |
* | **4. Lookup** | Index | Access the dispatch table | `dispatch(hash)` -> Returns index |
*
* #### Deep dive into "Packing" (Step 2):
* Imagine a 32-bit Integer as 4 empty boxes. We place each byte into a box,
* shifting them to the left so they don't overlap:
*
* ```text
* Slot 4 (24-31) | Slot 3 (16-23) | Slot 2 (8-15) | Slot 1 (0-7)
* --------------------------------------------------------------
* Empty | 'e' (101) | 'g' (103) | 'a' (97)
* ```
* *Each shift (<< 8, << 16, etc.) just pushes the byte into its assigned slot.*
*
* @param names The list of unique field names to index.
* @return Optimal hash parameters and whether extended key hashing is required at runtime.
*/
fun findPerfectHash(names: List<String>): PerfectHashConfig {
if (names.isEmpty()) {
return PerfectHashConfig(0, C.HASH_MULTIPLIER_START, 128, extendedKeyHash = false)
}
findPerfectHashInternal(names, useExtendedKeyHash = false)?.let { (shift, multiplier, tableSize) ->
return PerfectHashConfig(shift, multiplier, tableSize, extendedKeyHash = false)
}
findPerfectHashInternal(names, useExtendedKeyHash = true)?.let { (shift, multiplier, tableSize) ->
return PerfectHashConfig(shift, multiplier, tableSize, extendedKeyHash = true)
}
throw IllegalStateException(
C.STR_ERR_PERFECT_HASH_COLLISION_1 + names.joinToString() + C.STR_ERR_PERFECT_HASH_COLLISION_2
)
}
private fun findPerfectHashInternal(
names: List<String>,
useExtendedKeyHash: Boolean
): Triple<Int, Int, Int>? {
val rawBytes = names.map { it.encodeToByteArray() }
val hasCollisions = if (useExtendedKeyHash) {
true
} else {
detectPrefixLengthCollisions(rawBytes)
}
val tableSizes = listOf(128, 256, 512, 1024, 2048, 4096, 8192)
for (tableSize in tableSizes) {
val tableMask = tableSize - 1
for (multiplier in C.HASH_MULTIPLIER_START..C.HASH_MULTIPLIER_LIMIT step C.HASH_MULTIPLIER_STEP) {
for (shift in 0..C.HASH_SHIFT_LIMIT) {
val dispatch = IntArray(tableSize) { -1 }
var collision = false
for (index in rawBytes.indices) {
val bytes = rawBytes[index]
if (bytes.isNotEmpty()) {
val key = computeDispatchKey(bytes, hasCollisions)
val hash = ((key * multiplier + bytes.size) shr shift) and tableMask
if (dispatch[hash] == -1) {
dispatch[hash] = index
} else {
collision = true
break
}
}
}
if (!collision) {
return Triple(shift, multiplier, tableSize)
}
}
}
}
return null
}
private fun detectPrefixLengthCollisions(rawBytes: List<ByteArray>): Boolean {
val seen = HashSet<Long>()
for (bytes in rawBytes) {
if (bytes.isNotEmpty()) {
var mask = 0L
if (bytes.size >= C.VAL_ONE) mask = mask or (bytes[C.VAL_ZERO].toLong() and C.LONG_BYTE_MASK)
if (bytes.size >= C.VAL_TWO) mask = mask or ((bytes[C.VAL_ONE].toLong() and C.LONG_BYTE_MASK) shl C.BIT_SHIFT_8)
if (bytes.size >= C.VAL_THREE) mask = mask or ((bytes[C.VAL_TWO].toLong() and C.LONG_BYTE_MASK) shl C.BIT_SHIFT_16)
if (bytes.size >= C.VAL_FOUR) mask = mask or ((bytes[C.VAL_THREE].toLong() and C.LONG_BYTE_MASK) shl C.BIT_SHIFT_24)
val packed = mask or (bytes.size.toLong() shl C.BIT_SHIFT_32)
if (!seen.add(packed)) {
return true
}
}
}
return false
}
private fun computeDispatchKey(bytes: ByteArray, hasCollisions: Boolean): Int {
var key = 0
if (bytes.size >= C.VAL_ONE) {
key = key or (bytes[C.VAL_ZERO].toInt() and C.BYTE_MASK)
}
if (bytes.size >= C.VAL_TWO) {
key = key or ((bytes[C.VAL_ONE].toInt() and C.BYTE_MASK) shl C.BIT_SHIFT_8)
}
if (bytes.size >= C.VAL_THREE) {
key = key or ((bytes[C.VAL_TWO].toInt() and C.BYTE_MASK) shl C.BIT_SHIFT_16)
}
if (bytes.size >= C.VAL_FOUR) {
key = key or ((bytes[C.VAL_THREE].toInt() and C.BYTE_MASK) shl C.BIT_SHIFT_24)
}
if (hasCollisions) {
var ci = C.VAL_FOUR
while (ci < bytes.size) {
key = key * C.COLLISION_HASH_MULTIPLIER + (bytes[ci].toInt() and C.BYTE_MASK)
ci++
}
}
return key
}
}