Testei alguns algoritmos diferentes, medindo a velocidade e o número de colisões.
Eu usei três conjuntos de chaves diferentes:
Para cada corpus, foi registrado o número de colisões e o tempo médio gasto em hash.
Eu testei:
Resultados
Cada resultado contém o tempo médio de hash e o número de colisões
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 :
As colisões realmente acontecem?
Sim. Comecei a escrever meu programa de teste para ver se as colisões de hash realmente acontecem - e não são apenas uma construção teórica. Eles realmente acontecem:
Colisões FNV-1
creamwove
colide com quists
Colisões FNV-1a
costarring
colide com liquid
declinate
colide com macallums
altarage
colide com zinke
altarages
colide com zinkes
Colisões Murmur2
cataract
colide com periti
roquette
colide com skivie
shawl
colide com stormbound
dowlases
colide com tramontane
cricketings
colide com twanger
longans
colide com whigs
Colisões DJB2
hetairas
colide com mentioner
heliotropes
colide com neurospora
depravement
colide com serafins
stylist
colide com subgenera
joyful
colide com synaphea
redescribed
colide com urites
dram
colide com vivency
Colisões DJB2a
haggadot
colide com loathsomenesses
adorablenesses
colide com rentability
playwright
colide com snush
playwrighting
colide com snushing
treponematoses
colide com waterbeds
Colisões CRC32
codding
colide com gnu
exhibiters
colide com schlager
Colisões SuperFastHash
dahabiah
colide com drapability
encharm
colide com enclave
grahams
colide com gramary
- ... corta 79 colisões ...
night
colide com vigil
nights
colide com vigils
finks
colide com vinic
Randomnessification
A outra medida subjetiva é a distribuição aleatória dos hashes. O mapeamento das HashTables resultantes mostra a distribuição uniforme dos dados. Todas as funções hash mostram boa distribuição ao mapear linearmente a tabela:
Ou como um mapa de Hilbert (o XKCD é sempre relevante ):
Exceto quando hash cadeias numéricas ( "1"
, "2"
, ..., "216553"
) (por exemplo, zip codes ), onde os padrões começam a surgir na maioria dos algoritmos de hash:
SDBM :
DJB2a :
FNV-1 :
Todos, exceto o FNV-1a , que ainda parecem bastante aleatórios para mim:
Na verdade, Murmur2 parece ter ainda melhor aleatoriedade com Numbers
que FNV-1a
:
Quando olho para o FNV-1a
mapa "número", acho que vejo padrões verticais sutis. Com Murmur, não vejo nenhum padrão. O que você acha?
O extra *
na tabela indica quão ruim é a aleatoriedade. Com FNV-1a
sendo o melhor e DJB2x
ser o pior:
Murmur2: .
FNV-1a: .
FNV-1: ▪
DJB2: ▪▪
DJB2a: ▪▪
SDBM: ▪▪▪
SuperFastHash: .
CRC: ▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪
Loselose: ▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪
▪
▪▪▪▪▪▪▪▪▪▪▪▪▪
▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪
▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪
Originalmente, escrevi este programa para decidir se eu precisava me preocupar com colisões: sim.
E, em seguida, tornou-se garantir que as funções de hash fossem suficientemente aleatórias.
Algoritmo FNV-1a
O hash FNV1 vem em variantes que retornam hashes de 32, 64, 128, 256, 512 e 1024 bits.
O algoritmo FNV-1a é:
hash = FNV_offset_basis
for each octetOfData to be hashed
hash = hash xor octetOfData
hash = hash * FNV_prime
return hash
Onde as constantes FNV_offset_basis
e FNV_prime
dependem do tamanho do hash de retorno desejado:
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
Veja a página principal do FNV para detalhes.
Todos os meus resultados estão com a variante de 32 bits.
FNV-1 melhor que FNV-1a?
Não. O FNV-1a está bem melhor. Houve mais colisões com o FNV-1a ao usar a palavra em inglês corpus:
Hash Word Collisions
====== ===============
FNV-1 1
FNV-1a 4
Agora compare letras minúsculas e maiúsculas:
Hash lowercase word Collisions UPPERCASE word collisions
====== ========================= =========================
FNV-1 1 9
FNV-1a 4 11
Nesse caso, o FNV-1a não é "400%" pior que o FN-1, apenas 20% pior.
Penso que o mais importante é que existem duas classes de algoritmos quando se trata de colisões:
- colisões raras : FNV-1, FNV-1a, DJB2, DJB2a, SDBM
- colisões comuns : SuperFastHash, Loselose
E ainda há a distribuição uniforme dos hashes:
- excelente distribuição: Murmur2, FNV-1a, SuperFastHas
- excelente distribuição: FNV-1
- boa distribuição: SDBM, DJB2, DJB2a
- distribuição horrível: Loselose
Atualizar
Murmúrio? Claro, por que não
Atualizar
@whatshisname imaginou como seria o desempenho de um CRC32 , acrescentando números à tabela.
CRC32 é muito bom . Poucas colisões, porém mais lentas, e a sobrecarga de uma tabela de pesquisa de 1k.
Cortar todas as coisas erradas sobre a distribuição de CRC - meu mal
Até hoje eu usava o FNV-1a como meu algoritmo de hash de tabela de hash de fato . Mas agora estou mudando para o Murmur2:
- Mais rápido
- Melhor aleatorização de todas as classes de entrada
E eu realmente, realmente espero que haja algo de errado com o SuperFastHash
algoritmo que eu encontrei ; é muito ruim ser tão popular quanto é.
Update: A partir da página inicial MurmurHash3 no Google :
(1) - O SuperFastHash possui propriedades de colisão muito ruins, que foram documentadas em outros lugares.
Então acho que não sou só eu.
Atualização: eu percebi por que Murmur
é mais rápido que os outros. MurmurHash2 opera em quatro bytes de cada vez. A maioria dos algoritmos é byte a byte :
for each octet in Key
AddTheOctetToTheHash
Isso significa que, à medida que as teclas ficam mais longas, o Murmur tem a chance de brilhar.
Atualizar
Uma publicação oportuna de Raymond Chen reitera o fato de que os GUIDs "aleatórios" não devem ser usados por sua aleatoriedade. Eles, ou um subconjunto deles, não são adequados como chave de hash:
Mesmo o algoritmo GUID da versão 4 não é garantido como imprevisível, porque o algoritmo não especifica a qualidade do gerador de números aleatórios. O artigo da Wikipedia para GUID contém pesquisas primárias que sugerem que GUIDs futuros e anteriores podem ser previstos com base no conhecimento do estado do gerador de números aleatórios, já que o gerador não é criptograficamente forte.
Aleatoriedade não é o mesmo que evitar colisões; é por isso que seria um erro tentar inventar seu próprio algoritmo de "hash" usando um subconjunto de um guia "aleatório":
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 : Mais uma vez, coloquei "GUID aleatório" entre aspas, porque é a variante "aleatória" de GUIDs. Uma descrição mais precisa seria Type 4 UUID
. Mas ninguém sabe o que são os tipos 4 ou 1, 3 e 5. Portanto, é mais fácil chamá-los de GUIDs "aleatórios".
Todas as palavras em inglês mirrors