it-swarm.it

Quale algoritmo di hashing è il migliore per unicità e velocità?

Quale algoritmo di hashing è il migliore per unicità e velocità? Esempi (buoni) usi includono dizionari hash.

So che ci sono cose come SHA-256 e simili, ma questi algoritmi sono progettati per essere sicuro , che di solito significa che sono più lenti degli algoritmi che sono meno unici. Voglio un algoritmo di hash progettato per essere veloce, ma rimanere abbastanza unico per evitare collisioni.

1444
Earlz

Ho testato alcuni algoritmi diversi, misurando la velocità e il numero di collisioni.

Ho usato tre diversi set di chiavi:

Per ciascun corpus, è stato registrato il numero di collisioni e il tempo medio impiegato per l'hash.

Ho testato:

Risultati

Ogni risultato contiene il tempo di hash medio e il numero di collisioni

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

Note :

Le collisioni avvengono effettivamente?

Sì. Ho iniziato a scrivere il mio programma di test per vedere se le hash collisioni in realtà accadono - e non sono solo un costrutto teorico. Succedono davvero:

Collisioni FNV-1

  • creamwove si scontra con quists

Collisioni FNV-1a

  • costarring si scontra con liquid
  • declinate si scontra con macallums
  • altarage si scontra con zinke
  • altarages si scontra con zinkes

Collisioni Murmur2

  • cataract si scontra con periti
  • roquette si scontra con skivie
  • shawl si scontra con stormbound
  • dowlases si scontra con tramontane
  • cricketings si scontra con twanger
  • longans si scontra con whigs

Collisioni DJB2

  • hetairas si scontra con mentioner
  • heliotropes si scontra con neurospora
  • depravement si scontra con serafins
  • stylist si scontra con subgenera
  • joyful si scontra con synaphea
  • redescribed si scontra con urites
  • dram si scontra con vivency

Collisioni DJB2a

  • haggadot si scontra con loathsomenesses
  • adorablenesses si scontra con rentability
  • playwright si scontra con snush
  • playwrighting si scontra con snushing
  • treponematoses si scontra con waterbeds

Collisioni CRC32

  • codding si scontra con gnu
  • exhibiters si scontra con schlager

Collisioni SuperFastHash

  • dahabiah si scontra con drapability
  • encharm si scontra con enclave
  • grahams si scontra con gramary
  • ... taglia 79 collisioni ...
  • night si scontra con vigil
  • nights si scontra con vigils
  • finks si scontra con vinic

Randomnessification

L'altra misura soggettiva è la distribuzione casuale degli hash. La mappatura delle tabelle hash risultanti mostra la distribuzione uniforme dei dati. Tutte le funzioni hash mostrano una buona distribuzione quando si mappa la tabella in modo lineare:

Enter image description here

O come Hilbert Map ( XKCD è sempre rilevante ):

Enter image description here

Tranne quando le stringhe di numeri di hashing ("1", "2", ..., "216553") (ad esempio codici postali ), in cui i modelli iniziano a emergere nella maggior parte degli algoritmi di hashing:

[~ ~ #] sdbm [~ ~ #] :

Enter image description here

DJB2a :

Enter image description here

FNV-1 :

Enter image description here

Tutti tranne FNV-1a , che mi sembrano ancora abbastanza casuali:

Enter image description here

In effetti, Murmur2 sembra avere una casualità ancora migliore con Numbers rispetto a FNV-1a:

Enter image description here

Quando guardo il FNV-1a "numero" mappa, io penso vedo sottili schemi verticali. Con Murmur non vedo affatto schemi. Cosa ne pensi?


Il extra * nella tabella indica quanto sia grave la casualità. Con FNV-1a è il migliore e DJB2x è il peggiore:

      Murmur2: .
       FNV-1a: .
        FNV-1: ▪
         DJB2: ▪▪
        DJB2a: ▪▪
         SDBM: ▪▪▪
SuperFastHash: .
          CRC: ▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪
     Loselose: ▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪
                                        ▪
                                 ▪▪▪▪▪▪▪▪▪▪▪▪▪
                        ▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪
          ▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪

Inizialmente ho scritto questo programma per decidere se dovevo anche preoccuparti riguardo alle collisioni: lo faccio.

E poi si è verificato che le funzioni hash fossero sufficientemente casuali.

Algoritmo FNV-1a

L'hash FNV1 è disponibile in varianti che restituiscono hash a 32, 64, 128, 256, 512 e 1024 bit.

L'algoritmo FNV-1a è:

hash = FNV_offset_basis
for each octetOfData to be hashed
    hash = hash xor octetOfData
    hash = hash * FNV_prime
return hash

Dove le costanti FNV_offset_basis e FNV_prime dipende dalla dimensione dell'hash di ritorno che desideri:

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

Vedi la pagina FNV principale per i dettagli.

Tutti i miei risultati sono con la variante a 32 bit.

FNV-1 meglio di FNV-1a?

No. FNV-1a è tutto meglio. Ci sono state più collisioni con FNV-1a quando si utilizzava il corpus Word inglese:

Hash    Word Collisions
======  ===============
FNV-1   1
FNV-1a  4

Ora confronta lettere minuscole e maiuscole:

Hash    lowercase Word Collisions  UPPERCASE Word collisions
======  =========================  =========================
FNV-1   1                          9
FNV-1a  4                          11

In questo caso FNV-1a non è "400%" peggiore di FN-1, solo il 20% peggiore.

Penso che la cosa più importante sia che ci sono due classi di algoritmi quando si tratta di collisioni:

  • collisioni rare : FNV-1, FNV-1a, DJB2, DJB2a, SDBM
  • collisioni comuni : SuperFastHash, Loselose

E poi c'è la distribuzione uniforme degli hash:

  • distribuzione eccezionale: Murmur2, FNV-1a, SuperFastHas
  • distribuzione eccellente: FNV-1
  • buona distribuzione: SDBM, DJB2, DJB2a
  • distribuzione orribile: Loselose

Update

Mormorio? Certo, perché no


Update

@whatshisname si chiedeva come avrebbe funzionato un CRC32 , aggiungendo numeri alla tabella.

CRC32 è abbastanza buono. Poche collisioni, ma più lente, e il sovraccarico di una tabella di ricerca 1k.

Taglia tutte le cose errate sulla distribuzione CRC - il mio male


Fino ad oggi avrei usato FNV-1a come mio algoritmo di hash table di fatto hash-table. Ma ora sto passando a Murmur2:

  • Più veloce
  • Meglio randomnessification di tutte le classi di input

E io davvero, davvero spero che ci sia qualcosa di sbagliato nell'algoritmo SuperFastHash che ho trovato ; è un peccato essere così popolare come è.

Aggiornamento: Da la homepage di MurmurHash3 su Google :

(1) - SuperFastHash ha proprietà di collisione molto scarse, che sono state documentate altrove.

Quindi suppongo che non sono solo io.

Aggiornamento: Ho capito perché Murmur è più veloce degli altri. MurmurHash2 funziona su quattro byte alla volta. La maggior parte degli algoritmi sono byte per byte:

for each octet in Key
   AddTheOctetToTheHash

Ciò significa che man mano che le chiavi si allungano, il soffio ha la possibilità di brillare.


Update

I GUID sono progettati per essere unici, non casuali

Un post tempestivo di Raymond Chen ribadisce che "random" GUID non sono pensati per essere utilizzati per la loro casualità. Loro, o un loro sottoinsieme, non sono adatti come chiave hash:

Anche la versione 4 GUID non è garantito come imprevedibile, poiché l'algoritmo non specifica la qualità del generatore di numeri casuali. L'articolo di Wikipedia per GUID contiene ricerche primarie che suggeriscono che i GUID futuri e precedenti possono essere previsti in base alla conoscenza dello stato del generatore di numeri casuali, poiché il generatore non è crittograficamente forte.

La casualità non è la stessa come evitare le collisioni; ed è per questo che sarebbe un errore provare a inventare il proprio algoritmo di "hashing" prendendo un sottoinsieme di una guida "casuale":

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 : Ancora una volta, ho inserito "GUID casuale" tra virgolette, perché è la variante "casuale" dei GUID. Una descrizione più accurata sarebbe Type 4 UUID. Ma nessuno sa cosa siano i tipi 4 o 1, 3 e 5. Quindi è più semplice chiamarli GUID "casuali".

Tutti gli specchi di parole inglesi

2530
Ian Boyd

Se vuoi creare una mappa hash da un dizionario immutabile, potresti prendere in considerazione la creazione di hashing perfetto https://en.wikipedia.org/wiki/Perfect_hash_function - durante la costruzione della funzione hash e tabella hash, puoi garantire, per un dato set di dati, che non ci saranno collisioni.

61
Damien

Qui è un elenco di funzioni hash, ma la versione breve è:

Se vuoi solo avere una buona funzione hash e non puoi aspettare, djb2 è una delle migliori funzioni hash di stringa che conosco. Ha un'eccellente distribuzione e velocità su molti diversi set di chiavi e dimensioni della tabella

unsigned long
hash(unsigned char *str)
{
    unsigned long hash = 5381;
    int c;

    while (c = *str++)
        hash = ((hash << 5) + hash) + c; /* hash * 33 + c */

    return hash;
}
34
Dean Harding

CityHash di Google è l'algoritmo che stai cercando. Non è buono per la crittografia ma è buono per generare hash unici.

Leggi blog per maggiori dettagli e il codice è disponibile qui .

CityHash è scritto in C++. C'è anche un semplice porta C .

Circa supporto a 32 bit:

Tutte le funzioni di CityHash sono ottimizzate per processori a 64 bit. Detto questo, verranno eseguiti (ad eccezione di quelli nuovi che utilizzano SSE4.2) in codice a 32 bit. Non saranno molto veloci però. Potresti voler usare Murmur o qualcos'altro nel codice a 32 bit.

29
Vipin Parakkat

Ho tracciato un confronto a breve velocità di diversi algoritmi di hashing durante i file di hashing.

I singoli grafici differiscono solo leggermente nel metodo di lettura e possono essere ignorati qui, poiché tutti i file sono stati memorizzati in un tmpfs. Pertanto, il punto di riferimento non era vincolato all'IO se ti stai chiedendo.

Gli algoritmi includono: SpookyHash, CityHash, Murmur3, MD5, SHA{1,256,512}.

Conclusioni:

  • Le funzioni hash non crittografiche come Murmur3, Cityhash e Spooky sono piuttosto vicine tra loro. Si dovrebbe notare che Cityhash potrebbe essere più veloce su CPU con istruzione SSE 4.2s CRC, che la mia CPU non ha. SpookyHash era nel mio caso sempre un po 'prima di CityHash.
  • MD5 sembra essere un buon compromesso quando si usano le funzioni di hash crittografico, sebbene SHA256 possa essere più sicuro per vulnerabilità di collisione di MD5 e SHA1.
  • La complessità di tutti gli algoritmi è lineare, il che non sorprende, dal momento che funzionano in senso antiorario. (Volevo vedere se il metodo di lettura fa la differenza, quindi puoi semplicemente confrontare i valori più giusti).
  • SHA256 era più lento di SHA512.
  • Non ho studiato la casualità delle funzioni hash. Ma qui è un buon confronto delle funzioni hash che mancano in Ian Boyds answer . Questo sottolinea che CityHash ha dei problemi in casi d'angolo.

La fonte utilizzata per i grafici:

21
Sahib

Gli algoritmi SHA (incluso SHA-256) sono progettato per essere veloce.

In effetti, la loro velocità può essere un problema a volte. In particolare, una tecnica comune per l'archiviazione di un token derivato da password consiste nell'eseguire un algoritmo hash veloce standard 10.000 volte (memorizzando l'hash dell'hash dell'hash dell'hash della ... password).

#!/usr/bin/env Ruby
require 'securerandom'
require 'digest'
require 'benchmark'

def run_random_digest(digest, count)
  v = SecureRandom.random_bytes(digest.block_length)
  count.times { v = digest.digest(v) }
  v
end

Benchmark.bmbm do |x|
  x.report { run_random_digest(Digest::SHA256.new, 1_000_000) }
end

Produzione:

Rehearsal ------------------------------------
   1.480000   0.000000   1.480000 (  1.391229)
--------------------------- total: 1.480000sec

       user     system      total        real
   1.400000   0.000000   1.400000 (  1.382016)
18
yfeldblum

So che ci sono cose come SHA-256 e simili, ma questi algoritmi sono progettati per essere sicuri , che di solito significa che sono più lenti degli algoritmi che sono meno unici.

L'ipotesi che le funzioni di hash crittografiche siano più uniche è errata, e in effetti si può dimostrare che nella pratica è spesso arretrato. In verità:

  1. Le funzioni hash crittografiche dovrebbero idealmente essere indistinguibili da casuali ;
  2. Ma con funzioni hash non crittografiche, è desiderabile che interagiscano in modo favorevole con input probabili .

Ciò significa che una funzione hash non crittografica potrebbe avere un numero inferiore di collisioni rispetto a una crittografia per un set di dati "valido": set di dati per cui è stato progettato .

Possiamo effettivamente dimostrarlo con i dati nella risposta di Ian Boyd e un po 'di matematica: il problema del compleanno . La formula per il numero previsto di coppie in collisione se scegli n numeri interi a caso dall'insieme [1, d] è questo (tratto da Wikipedia):

n - d + d * ((d - 1) / d)^n

Collegando n = 216.553 e d = 2 ^ 32 otteniamo circa 5.5 collisioni previste . I test di Ian mostrano principalmente risultati in quel quartiere, ma con una eccezionale eccezione: la maggior parte delle funzioni ha ottenuto zero collisioni nei test dei numeri consecutivi. La probabilità di scegliere casualmente 216.553 numeri a 32 bit e ottenere zero collisioni è di circa lo 0,43%. E questo è solo per una funzione: qui abbiamo cinque famiglie di funzioni hash distinte con zero collisioni!

Quindi quello che stiamo vedendo qui è che gli hash che Ian ha testato interagiscono favorevolmente con il set di dati di numeri consecutivi — cioè, si stanno disperdendo input minimamente diversi più ampiamente di quanto sarebbe una funzione di hash crittografica ideale. (Nota a margine: questo significa che la valutazione grafica di Ian secondo cui FNV-1a e MurmurHash2 "gli sembrano casuali" nel set di dati numerici può essere smentita dai suoi stessi dati. Zero collisioni su un set di dati di quelle dimensioni, per entrambi funzioni hash, è sorprendentemente non casuale!)

Questa non è una sorpresa perché è un comportamento desiderabile per molti usi delle funzioni hash. Ad esempio, le chiavi della tabella hash sono spesso molto simili; La risposta di Ian menziona n problema che MSN aveva una volta con le tabelle hash del codice postale . Questo è un uso in cui l'evitamento alle collisioni su probabilmente input vince su un comportamento simile a quello casuale.

Un altro confronto istruttivo qui è il contrasto negli obiettivi di progettazione tra CRC e le funzioni hash crittografiche:

  • CRC è progettato per rilevare errori risultanti da canali di comunicazione rumorosi , che possono essere probabilmente un piccolo numero di lanci di bit;
  • Gli hash crittografici sono progettati per catturare le modifiche apportate da aggressori malintenzionati , a cui sono assegnate risorse computazionali limitate ma arbitrariamente molta intelligenza.

Quindi per CRC è di nuovo buono avere meno collisioni che casuali in input minimamente diversi. Con gli hash crittografici, questo è un no-no!

15
sacundim

Usa SipHash . Ha molte proprietà desiderabili :

  • Rapido. Un'implementazione ottimizzata richiede circa 1 ciclo per byte.

  • Sicuro. SipHash è un potente PRF (funzione pseudocasuale). Ciò significa che è indistinguibile da una funzione casuale (a meno che non si conosca la chiave segreta a 128 bit). Quindi:

    • Non è necessario preoccuparsi che le sonde della tabella di hash diventino tempi lineari a causa delle collisioni. Con SipHash, sai che otterrai prestazioni di caso medio in media, indipendentemente dagli input.

    • Immunità agli attacchi denial of service basati sull'hash.

    • È possibile utilizzare SipHash (in particolare la versione con output a 128 bit) come MAC (Message Authentication Code). Se ricevi un messaggio e un tag SipHash e il tag è lo stesso dell'esecuzione di SipHash con la tua chiave segreta, allora sai che chiunque ha creato l'hash era anche in possesso della tua chiave segreta e che né il messaggio né il l'hash è stato modificato da allora.

10
Demi

Dipende dai dati che stai eseguendo l'hashing. Alcuni hash funzionano meglio con dati specifici come il testo. Alcuni algoritmi di hashing erano specificamente progettati per essere adatti a dati specifici.

Una volta Paul Hsieh ha fatto hash veloce . Elenca il codice sorgente e le spiegazioni. Ma era già stato battuto. :)

9
user712092

Java utilizza questo semplice algoritmo moltiplica e aggiungi:

Il codice hash per un oggetto String viene calcolato come

 s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]

usando int arithmetic, dove s[i] è il carattere i -th della stringa, n è la lunghezza della stringa e ^ indica esponenziazione. (Il valore di hash della stringa vuota è zero.)

Probabilmente ce ne sono di migliori là fuori, ma questo è abbastanza diffuso e sembra essere un buon compromesso tra velocità e unicità.

6
biziclop

Prima di tutto, perché devi implementare il tuo hash? Per la maggior parte delle attività dovresti ottenere buoni risultati con strutture di dati da una libreria standard, supponendo che sia disponibile un'implementazione (a meno che tu non lo stia facendo solo per la tua formazione).

Per quanto riguarda gli algoritmi di hashing, il mio preferito è FNV. 1

Ecco un esempio di implementazione della versione a 32 bit in C:

unsigned long int FNV_hash(void* dataToHash, unsigned long int length)
{
  unsigned char* p = (unsigned char *) dataToHash;
  unsigned long int h = 2166136261UL;
  unsigned long int i;

  for(i = 0; i < length; i++)
    h = (h * 16777619) ^ p[i] ;

  return h;
}
4
user17754