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 Numbers
que FNV-1a
:
Cuando miro el FNV-1a
mapa 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-1a
ser el mejor y DJB2x
ser 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_basis
y FNV_prime
dependen 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 SuperFastHash
algoritmo 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é Murmur
es 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