Fino a poco tempo fa la mia risposta sarebbe stata molto vicina a quella di Jon Skeet qui. Tuttavia, di recente ho avviato un progetto che utilizzava tabelle hash power-of-two, ovvero tabelle hash in cui le dimensioni della tabella interna sono 8, 16, 32, ecc. C'è una buona ragione per favorire le dimensioni dei numeri primi, ma lì sono anche alcuni vantaggi della potenza di due taglie.
E praticamente succhiato. Quindi, dopo un po 'di sperimentazione e ricerca, ho iniziato a rielaborare i miei hash con quanto segue:
public static int ReHash(int source)
{
unchecked
{
ulong c = 0xDEADBEEFDEADBEEF + (ulong)source;
ulong d = 0xE2ADBEEFDEADBEEF ^ c;
ulong a = d += c = c << 15 | c >> -15;
ulong b = a += d = d << 52 | d >> -52;
c ^= b += a = a << 26 | a >> -26;
d ^= c += b = b << 51 | b >> -51;
a ^= d += c = c << 28 | c >> -28;
b ^= a += d = d << 9 | d >> -9;
c ^= b += a = a << 47 | a >> -47;
d ^= c += b << 54 | b >> -54;
a ^= d += c << 32 | c >> 32;
a += d << 25 | d >> -25;
return (int)(a >> 1);
}
}
E poi la mia tabella hash power-of-two non ha più succhiato.
Questo però mi ha disturbato, perché quanto sopra non dovrebbe funzionare. O più precisamente, non dovrebbe funzionare se l'originale non GetHashCode()
era scadente in un modo molto particolare.
Ri-mescolare un hashcode non può migliorare un ottimo hashcode, perché l'unico effetto possibile è che introduciamo qualche altra collisione.
Ri-mescolare un codice hash non può migliorare un codice hash terribile, perché l'unico effetto possibile è cambiare ad esempio un gran numero di collisioni sul valore 53 in un gran numero di valore 18.3487.291.
Ri-mescolare un codice hash può solo migliorare un codice hash che ha funzionato almeno abbastanza bene nell'evitare le collisioni assolute in tutto il suo intervallo (2 32 valori possibili) ma malamente nell'evitare le collisioni quando il modulo è giù per l'uso effettivo in una tabella hash. Mentre il modulo più semplice di una tabella di potenza di due ha reso questo più evidente, stava anche avendo un effetto negativo con le più comuni tabelle dei numeri primi, che non era altrettanto ovvio (il lavoro extra nel rifacimento avrebbe superato il vantaggio , ma il vantaggio sarebbe ancora lì).
Modifica: stavo anche usando l'indirizzamento aperto, che avrebbe anche aumentato la sensibilità alla collisione, forse più del fatto che fosse un potere di due.
E beh, era inquietante quanto le string.GetHashCode()
implementazioni in .NET (o studio qui ) potessero essere migliorate in questo modo (sull'ordine dei test in esecuzione circa 20-30 volte più veloce a causa di minori collisioni) e più inquietante quanto i miei codici hash potrebbe essere migliorato (molto di più).
Tutte le implementazioni di GetHashCode () che avevo codificato in passato e che avevo effettivamente utilizzato come base per le risposte su questo sito, erano molto peggio di quanto avessi vissuto . Gran parte del tempo era "abbastanza buono" per gran parte degli usi, ma volevo qualcosa di meglio.
Quindi ho messo da parte quel progetto (era comunque un progetto pet) e ho iniziato a cercare rapidamente come produrre un codice hash ben distribuito in .NET.
Alla fine ho deciso di trasferire SpookyHash su .NET. In effetti il codice sopra è una versione rapida dell'uso di SpookyHash per produrre un output a 32 bit da un input a 32 bit.
Ora, SpookyHash non è un bel veloce da ricordare pezzo di codice. La mia porta è ancora meno perché ne ho tracciato molto a mano per una migliore velocità *. Ma è a questo che serve il riutilizzo del codice.
Quindi ho messo da parte quel progetto, perché proprio come il progetto originale aveva prodotto la domanda su come produrre un codice hash migliore, così quel progetto ha prodotto la domanda su come produrre un memcpy .NET migliore.
Poi sono tornato e ho prodotto molti sovraccarichi per alimentare facilmente quasi tutti i tipi nativi (tranne decimal
†) in un codice hash.
È veloce, per il quale Bob Jenkins merita gran parte del merito perché il suo codice originale da cui ho portato è ancora più veloce, specialmente su macchine a 64 bit per le quali l'algoritmo è ottimizzato ‡.
Il codice completo è disponibile su https://bitbucket.org/JonHanna/spookilysharp/src, ma considera che il codice sopra è una versione semplificata di esso.
Tuttavia, poiché ora è già stato scritto, è possibile utilizzarlo più facilmente:
public override int GetHashCode()
{
var hash = new SpookyHash();
hash.Update(field1);
hash.Update(field2);
hash.Update(field3);
return hash.Final().GetHashCode();
}
Prende anche valori seed, quindi se hai bisogno di gestire input non attendibili e vuoi proteggerti dagli attacchi Hash DoS puoi impostare un seed in base al tempo di attività o simile e rendere imprevedibili i risultati degli aggressori:
private static long hashSeed0 = Environment.TickCount;
private static long hashSeed1 = DateTime.Now.Ticks;
public override int GetHashCode()
{
//produce different hashes ever time this application is restarted
//but remain consistent in each run, so attackers have a harder time
//DoSing the hash tables.
var hash = new SpookyHash(hashSeed0, hashSeed1);
hash.Update(field1);
hash.Update(field2);
hash.Update(field3);
return hash.Final().GetHashCode();
}
* Una grande sorpresa in questo è che un metodo di rotazione allineato a mano ha restituito (x << n) | (x >> -n)
cose migliorate. Sarei stato sicuro che il jitter lo avrebbe sottolineato per me, ma la profilazione ha mostrato il contrario.
† decimal
non è nativo dal punto di vista .NET sebbene provenga dal C #. Il problema è che la propria GetHashCode()
tratta la precisione in modo significativo mentre la propria Equals()
no. Entrambe sono scelte valide, ma non mescolate in questo modo. Nell'implementazione della tua versione, devi scegliere di fare l'una o l'altra, ma non posso sapere quale vorresti.
‡ A titolo di confronto. Se utilizzato su una stringa, SpookyHash su 64 bit è considerevolmente più veloce rispetto string.GetHashCode()
a 32 bit, che è leggermente più veloce rispetto string.GetHashCode()
a 64 bit, che è considerevolmente più veloce di SpookyHash su 32 bit, sebbene sia ancora abbastanza veloce da essere una scelta ragionevole.
GetHashCode
. Spero che sarebbe utile per gli altri. Linee guida e regole per GetHashCode scritte da Eric Lippert