Probé algunos algoritmos diferentes, midiendo la velocidad y el número de colisiones.
Usé tres conjuntos de teclas diferentes:
Para cada corpus, se registró el número de colisiones y el tiempo promedio empleado en el hashing.
Probé:
Resultados
Cada resultado contiene el tiempo promedio de hash y el número de colisiones
Hash Lowercase Random UUID Numbers
============= ============= =========== ==============
Murmur 145 ns 259 ns 92 ns
6 collis 5 collis 0 collis
FNV-1a 152 ns 504 ns 86 ns
4 collis 4 collis 0 collis
FNV-1 184 ns 730 ns 92 ns
1 collis 5 collis 0 collis▪
DBJ2a 158 ns 443 ns 91 ns
5 collis 6 collis 0 collis▪▪▪
DJB2 156 ns 437 ns 93 ns
7 collis 6 collis 0 collis▪▪▪
SDBM 148 ns 484 ns 90 ns
4 collis 6 collis 0 collis**
SuperFastHash 164 ns 344 ns 118 ns
85 collis 4 collis 18742 collis
CRC32 250 ns 946 ns 130 ns
2 collis 0 collis 0 collis
LoseLose 338 ns - -
215178 collis
Notas :
¿Las colisiones suceden realmente?
Si. Comencé a escribir mi programa de prueba para ver si realmente ocurren colisiones de hash , y no son solo una construcción teórica. De hecho suceden:
Colisiones FNV-1
creamwove choca con quists
Colisiones FNV-1a
costarring choca con liquid
declinate choca con macallums
altarage choca con zinke
altarages choca con zinkes
Murmurio2 colisiones
cataract choca con periti
roquette choca con skivie
shawl choca con stormbound
dowlases choca con tramontane
cricketings choca con twanger
longans choca con whigs
Colisiones DJB2
hetairas choca con mentioner
heliotropes choca con neurospora
depravement choca con serafins
stylist choca con subgenera
joyful choca con synaphea
redescribed choca con urites
dram choca con vivency
DJB2a colisiones
haggadot choca con loathsomenesses
adorablenesses choca con rentability
playwright choca con snush
playwrighting choca con snushing
treponematoses choca con waterbeds
Colisiones CRC32
codding choca con gnu
exhibiters choca con schlager
Colisiones SuperFastHash
dahabiah choca con drapability
encharm choca con enclave
grahams choca con gramary
- ... corta 79 colisiones ...
night choca con vigil
nights choca con vigils
finks choca con vinic
Aleatorización
La otra medida subjetiva es la distribución aleatoria de los hashes. La asignación de las HashTables resultantes muestra cuán uniformemente se distribuyen los datos. Todas las funciones hash muestran una buena distribución al mapear la tabla linealmente:

O como un mapa de Hilbert ( XKCD siempre es relevante ):

Excepto cuando hash cadenas de números ( "1", "2", ..., "216553") (por ejemplo, códigos postales ), donde los patrones comienzan a surgir en la mayoría de los algoritmos de hash:
SDBM :

DJB2a :

FNV-1 :

Todos excepto FNV-1a , que todavía me parecen bastante aleatorios:

De hecho, Murmur2 parece tener una aleatoriedad aún mejor con Numbersque FNV-1a:

Cuando miro el FNV-1amapa de "números", creo que veo sutiles patrones verticales. Con Murmur no veo ningún patrón en absoluto. ¿Qué piensas?
El extra *en la tabla denota cuán mala es la aleatoriedad. Con FNV-1aser el mejor y DJB2xser el peor:
Murmur2: .
FNV-1a: .
FNV-1: ▪
DJB2: ▪▪
DJB2a: ▪▪
SDBM: ▪▪▪
SuperFastHash: .
CRC: ▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪
Loselose: ▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪
▪
▪▪▪▪▪▪▪▪▪▪▪▪▪
▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪
▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪
Originalmente escribí este programa para decidir si incluso tenía que preocuparme por colisiones: lo hago.
Y luego se convirtió en asegurarse de que las funciones hash fueran lo suficientemente aleatorias.
Algoritmo FNV-1a
El hash FNV1 viene en variantes que devuelven hashes de 32, 64, 128, 256, 512 y 1024 bits.
El algoritmo FNV-1a es:
hash = FNV_offset_basis
for each octetOfData to be hashed
hash = hash xor octetOfData
hash = hash * FNV_prime
return hash
Donde las constantes FNV_offset_basisy FNV_primedependen del tamaño de hash de retorno que desee:
Hash Size
===========
32-bit
prime: 2^24 + 2^8 + 0x93 = 16777619
offset: 2166136261
64-bit
prime: 2^40 + 2^8 + 0xb3 = 1099511628211
offset: 14695981039346656037
128-bit
prime: 2^88 + 2^8 + 0x3b = 309485009821345068724781371
offset: 144066263297769815596495629667062367629
256-bit
prime: 2^168 + 2^8 + 0x63 = 374144419156711147060143317175368453031918731002211
offset: 100029257958052580907070968620625704837092796014241193945225284501741471925557
512-bit
prime: 2^344 + 2^8 + 0x57 = 35835915874844867368919076489095108449946327955754392558399825615420669938882575126094039892345713852759
offset: 9659303129496669498009435400716310466090418745672637896108374329434462657994582932197716438449813051892206539805784495328239340083876191928701583869517785
1024-bit
prime: 2^680 + 2^8 + 0x8d = 5016456510113118655434598811035278955030765345404790744303017523831112055108147451509157692220295382716162651878526895249385292291816524375083746691371804094271873160484737966720260389217684476157468082573
offset: 1419779506494762106872207064140321832088062279544193396087847491461758272325229673230371772250864096521202355549365628174669108571814760471015076148029755969804077320157692458563003215304957150157403644460363550505412711285966361610267868082893823963790439336411086884584107735010676915
Vea la página principal de FNV para más detalles.
Todos mis resultados son con la variante de 32 bits.
FNV-1 mejor que FNV-1a?
No. FNV-1a es mucho mejor. Hubo más colisiones con FNV-1a al usar la palabra inglesa corpus:
Hash Word Collisions
====== ===============
FNV-1 1
FNV-1a 4
Ahora compare minúsculas y mayúsculas:
Hash lowercase word Collisions UPPERCASE word collisions
====== ========================= =========================
FNV-1 1 9
FNV-1a 4 11
En este caso, FNV-1a no es "400%" peor que FN-1, solo 20% peor.
Creo que lo más importante es que hay dos clases de algoritmos cuando se trata de colisiones:
- Colisiones raras : FNV-1, FNV-1a, DJB2, DJB2a, SDBM
- colisiones comunes : SuperFastHash, Loselose
Y luego está la distribución uniforme de los hashes:
- distribución sobresaliente: Murmur2, FNV-1a, SuperFastHas
- Excelente distribución: FNV-1
- buena distribución: SDBM, DJB2, DJB2a
- distribución horrible: Loselose
Actualizar
¿Murmullo? Seguro Por qué no
Actualizar
@whatshisname se preguntó cómo funcionaría un CRC32 , agregó números a la tabla.
CRC32 es bastante bueno . Pocas colisiones, pero más lentas, y la sobrecarga de una tabla de búsqueda de 1k.
Recorte todas las cosas erróneas sobre la distribución de CRC - my bad
Hasta hoy iba a usar FNV-1a como mi algoritmo de hash de tabla hash de facto . Pero ahora me estoy cambiando a Murmur2:
- Más rápido
- Mejor aleatorización de todas las clases de entrada.
Y realmente, realmente espero que haya algo mal con el SuperFastHashalgoritmo que encontré ; Es una pena ser tan popular como es.
Actualización: desde la página de inicio de MurmurHash3 en Google :
(1) - SuperFastHash tiene propiedades de colisión muy pobres, que se han documentado en otros lugares.
Así que supongo que no soy solo yo.
Actualización: me di cuenta de por qué Murmures más rápido que los demás. MurmurHash2 opera en cuatro bytes a la vez. La mayoría de los algoritmos son byte a byte :
for each octet in Key
AddTheOctetToTheHash
Esto significa que a medida que las teclas se alargan, Murmur tiene la oportunidad de brillar.
Actualizar
Una publicación oportuna de Raymond Chen reitera el hecho de que los GUID "aleatorios" no deben usarse para su aleatoriedad. Ellos, o un subconjunto de ellos, no son adecuados como una clave hash:
Incluso no se garantiza que el algoritmo GUID de la Versión 4 sea impredecible, porque el algoritmo no especifica la calidad del generador de números aleatorios. El artículo de Wikipedia para GUID contiene investigaciones primarias que sugieren que los GUID futuros y anteriores pueden predecirse basándose en el conocimiento del estado del generador de números aleatorios, ya que el generador no es criptográficamente fuerte.
Aleatoriedad no es lo mismo que evitar colisiones; por eso sería un error intentar inventar su propio algoritmo de "hashing" tomando algún subconjunto de un guid "aleatorio":
int HashKeyFromGuid(Guid type4uuid)
{
//A "4" is put somewhere in the GUID.
//I can't remember exactly where, but it doesn't matter for
//the illustrative purposes of this pseudocode
int guidVersion = ((type4uuid.D3 & 0x0f00) >> 8);
Assert(guidVersion == 4);
return (int)GetFirstFourBytesOfGuid(type4uuid);
}
Nota : Nuevamente, pongo "GUID aleatorio" entre comillas, porque es la variante "aleatoria" de GUID. Una descripción más precisa sería Type 4 UUID. Pero nadie sabe qué son los tipos 4 o 1, 3 y 5. Por lo tanto, es más fácil llamarlos GUID "aleatorios".
Todas las palabras inglesas reflejan