È possibile implementare una tabella hash ben distribuita senza usare l'operatore%?


11

Sto cercando di implementare una tabella hash veloce e ben distribuita in C #. Ho difficoltà a scegliere la mia funzione di vincolo hash che accetta un codice hash arbitrario e lo "vincola" in modo che possa essere utilizzato per indicizzare i bucket. Ci sono due opzioni che vedo finora:

  • Da un lato, puoi assicurarti che i tuoi bucket abbiano sempre un numero primo di elementi e, per vincolare l'hash, devi semplicemente modellarlo con il numero di bucket. Questo è, in effetti, ciò che fa il dizionario .NET . Il problema con questo approccio è che l'utilizzo di% è estremamente lento rispetto ad altre operazioni; se si guardano le tabelle di istruzioni della nebbia di Agner , idiv(che è il codice assembly che viene generato per%) ha una latenza di istruzione di ~ 25 cicli per i nuovi processori Intel. Confrontare questo a circa il 3 per mul, o 1 per ops bit per bit come and, oro xor.

  • D'altra parte, puoi avere il numero di bucket sempre una potenza di 2. Dovrai comunque calcolare il modulo dell'hash in modo da non tentare di indicizzare all'esterno dell'array, ma questa volta sarà meno costoso . Poiché per potenze di 2 % Nè giusto & (N - 1), il vincolo si riduce a un'operazione di mascheramento che richiede solo 1-2 cicli. Questo è fatto da Google Sparsehash . L'aspetto negativo di questo è che contiamo sugli utenti per fornire buoni hash; il mascheramento dell'hash essenzialmente interrompe parte dell'hash, quindi non stiamo più prendendo in considerazione tutti i bit dell'hash. Se l'hash dell'utente viene distribuito in modo non uniforme, ad esempio vengono compilati solo i bit più alti o i bit più bassi sono sempre gli stessi, questo approccio ha un tasso di collisioni molto più elevato.

Sto cercando un algoritmo che posso usare che abbia il meglio di entrambi i mondi: prende in considerazione tutti i bit dell'hash ed è anche più veloce dell'uso di%. Non deve necessariamente essere un modulo, solo qualcosa che è garantito essere nella gamma 0..N-1(dove N è la lunghezza dei bucket) e ha una distribuzione uniforme per tutti gli slot. Esiste un tale algoritmo?

Grazie dell'aiuto.


1
Cerca l' effetto valanga e la spiegazione in murmurhash3 (smhasher) . Tuttavia, il punto fondamentale della tua domanda non viene affrontato adottando una migliore funzione hash. Invece, si tratta del motivo per cui gli utenti non adottano la stessa migliore funzione di hash in primo luogo e una richiesta di contromisure (come se gli utenti fossero pigri e pigri).
rwong,


Per modulo veloce (2^N +/- 1), vedere stackoverflow.com/questions/763137/...
rwong

@rwong Mi dispiace, ma non sono sicuro di cosa abbia a che fare il tuo commento con il mio post. Non controllo l'hash fornito dall'utente, quindi non sto cercando una funzione hash migliore. Inoltre non capisco cosa intendi per "utenti maliziosamente pigri".
James Ko,

4
Se la funzione hash è scarsa, non c'è nulla che l'implementatore della tabella hash possa fare per "correggere" la distribuzione scadente. Modulo un numero primo non ripara un hash scadente. Considera una funzione hash che produce come output, multipli di un numero primo. Ho riscontrato un tale problema nel vero codice di produzione.
Frank Hileman,

Risposte:


9

Le implementazioni moderne della tabella hash non usano la funzione modulo. Spesso usano la potenza di tavoli di due dimensioni e tagliano pezzi non necessari. Una funzione hash ideale lo consentirebbe. L'uso del modulo combinato con le dimensioni della tabella dei numeri primi è sorto nei giorni in cui le funzioni hash erano generalmente scarse, come spesso accade nello sviluppo di .net. Consiglio di leggere SipHash , una moderna funzione hash, quindi di leggere alcune altre funzioni moderne, come xxHash .

Dovrei spiegare perché le funzioni hash .net sono spesso scadenti. In .net, i programmatori sono spesso costretti a implementare funzioni hash sovrascrivendo GetHashcode. Ma .net non fornisce gli strumenti necessari per garantire che le funzioni create dal programmatore siano di alta qualità, vale a dire:

  • incapsulamento dello stato hash in una struttura o classe
  • hash "aggiungi" funzioni, che aggiungono nuovi dati allo stato hash (aggiungi un array di byte, o un doppio, per esempio)
  • una funzione di "finalizzazione" dell'hash, per produrre la valanga
  • incapsulamento del risultato hash - in .net si ottiene una scelta, un intero con segno a 32 bit.

Per ulteriori informazioni sull'uso di un risultato della funzione hash come indice di una tabella hash, vedere le definizioni di forme universali di hash in questo documento: hashing universale più veloce a 64 bit utilizzando le moltiplicazioni carry-less


3

Per usare AND pur mantenendo tutti i bit, usa anche XOR.

Per un esempio temp = (hash & 0xFFFF) ^ ( hash >> 16); index = (temp & 0xFF) ^ (temp >> 8);,.

Per questo esempio, non esiste un modulo e tutti i 32 bit di hasheffetto sono gli 8 bit index. Tuttavia, se è più veloce di DIV è qualcosa che dipende da troppi fattori e in alcuni casi può essere facilmente più lento di DIV (ad esempio hash grande e indice minuscolo).


Questo sarà sempre più veloce di DIV / IDIV, tuttavia non credo che risponda alla mia domanda: indexsarà nell'intervallo [0..255]. Ho bisogno di qualcosa nella gamma [0..n-1], dov'è nil numero di secchi.
James Ko,

@JamesKo Ma se stai implementando un dizionario, controlli anche il numero di bucket (fino a un certo punto). Quindi, invece dei numeri primi, potresti scegliere potenze di due. (Se farlo sarebbe effettivamente una buona idea, non posso dirtelo.)
svick

@svick Per potenze di 2 potremmo fare una semplice operazione di maschera. Come accennato nella domanda, sto cercando un modo economico per farlo con i numeri primi, quindi anche gli hash mal distribuiti sono sistemati.
James Ko,

1

Puoi trarre vantaggio dal fatto che molti numeri primi hanno un inverso moltiplicativo modulare. Vedere questo articolo . Hai soddisfatto uno dei vincoli rendendo il tuo indice bucket e il modulo 2 ^ n, che sono intrinsecamente primi.

L'articolo descrive l'algoritmo per trovare un numero tale che moltiplicando per quel numero e ignorando l'overflow, si otterrà lo stesso risultato come se si fosse diviso per la dimensione dell'indice bucket.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.