Jusqu'à récemment, ma réponse aurait été très proche de celle de Jon Skeet ici. Cependant, j'ai récemment lancé un projet qui utilisait des tables de hachage avec puissance de deux, c'est-à-dire des tables de hachage où la taille de la table interne est de 8, 16, 32, etc. Il y a une bonne raison de privilégier les tailles de nombre premier, mais sont également des avantages pour les tailles à deux.
Et c'est à peu près nul. Donc, après un peu d'expérimentation et de recherche, j'ai commencé à retailler mes hachages avec les éléments suivants:
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);
}
}
Et puis ma table de hachage de puissance de deux n'a plus sucé.
Cela m'a toutefois dérangé, car ce qui précède ne devrait pas fonctionner. Ou plus précisément, cela ne devrait fonctionner que si l'original GetHashCode()
était médiocre d'une manière très particulière.
Re-mélanger un hashcode ne peut pas améliorer un excellent hashcode, car le seul effet possible est que nous introduisons quelques collisions supplémentaires.
Re-mélanger un code de hachage ne peut pas améliorer un terrible code de hachage, car le seul effet possible est que nous changeons par exemple un grand nombre de collisions sur la valeur 53 en un grand nombre de valeur 18 348 27991.
Re-mélanger un code de hachage ne peut qu'améliorer un code de hachage qui a au moins assez bien réussi à éviter les collisions absolues sur toute sa plage (2 32 valeurs possibles) mais mal à éviter les collisions lorsqu'il est modulé pour une utilisation réelle dans une table de hachage. Bien que le module plus simple d'une table de puissance de deux ait rendu cela plus évident, il avait également un effet négatif avec les tables de nombres premiers les plus courantes, ce n'était tout simplement pas aussi évident (le travail supplémentaire de ressassement l'emporterait sur l'avantage , mais l'avantage serait toujours là).
Edit: J'utilisais également l'adressage ouvert, ce qui aurait également augmenté la sensibilité à la collision, peut-être plus que le fait qu'il s'agissait d'une puissance de deux.
Et bien, cela perturbait la façon dont les string.GetHashCode()
implémentations dans .NET (ou étudiez ici ) pouvaient être améliorées de cette façon (dans l'ordre des tests qui s'exécutaient environ 20 à 30 fois plus rapidement en raison de moins de collisions) et plus inquiétant combien mes propres codes de hachage pourrait être amélioré (bien plus que cela).
Toutes les implémentations de GetHashCode () que j'avais codées dans le passé, et en fait utilisées comme base de réponses sur ce site, étaient bien pires que je n'en avais traversé . La plupart du temps, c'était "assez bien" pour la plupart des utilisations, mais je voulais quelque chose de mieux.
J'ai donc mis ce projet de côté (c'était un projet familier de toute façon) et j'ai commencé à chercher comment produire rapidement un bon code de hachage bien distribué dans .NET.
À la fin, j'ai décidé de porter SpookyHash sur .NET. En effet, le code ci-dessus est une version rapide de l'utilisation de SpookyHash pour produire une sortie 32 bits à partir d'une entrée 32 bits.
Maintenant, SpookyHash n'est pas un bon morceau de code rapide à retenir. Mon port est encore moins parce que j'en ai aligné beaucoup pour une meilleure vitesse *. Mais c'est à cela que sert la réutilisation du code.
Ensuite, j'ai mis ce projet de côté, car tout comme le projet d'origine avait posé la question de savoir comment produire un meilleur code de hachage, ce projet a posé la question de savoir comment produire une meilleure mémoire .NET.
Puis je suis revenu et j'ai produit beaucoup de surcharges pour alimenter facilement à peu près tous les types natifs (sauf decimal
†) dans un code de hachage.
C'est rapide, pour lequel Bob Jenkins mérite le plus de crédit parce que son code d'origine à partir duquel je l'ai porté est encore plus rapide, en particulier sur les machines 64 bits pour lesquelles l'algorithme est optimisé ‡.
Le code complet peut être consulté sur https://bitbucket.org/JonHanna/spookilysharp/src mais considérez que le code ci-dessus en est une version simplifiée.
Cependant, comme il est déjà écrit, on peut l'utiliser plus facilement:
public override int GetHashCode()
{
var hash = new SpookyHash();
hash.Update(field1);
hash.Update(field2);
hash.Update(field3);
return hash.Final().GetHashCode();
}
Il prend également des valeurs de graine, donc si vous avez besoin de traiter des entrées non fiables et que vous souhaitez vous protéger contre les attaques Hash DoS, vous pouvez définir une graine basée sur la disponibilité ou similaire, et rendre les résultats imprévisibles pour les attaquants:
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();
}
* Une grande surprise est que cette méthode de rotation en ligne à la main a permis d' (x << n) | (x >> -n)
améliorer les choses. J'aurais été sûr que la gigue aurait souligné cela pour moi, mais le profilage a montré le contraire.
† decimal
n'est pas natif du point de vue .NET bien qu'il provienne du C #. Le problème avec cela est que son propre GetHashCode()
considère la précision comme significative tandis que le sien Equals()
ne le fait pas. Les deux sont des choix valables, mais pas mélangés comme ça. Lors de l'implémentation de votre propre version, vous devez choisir de faire l'une ou l'autre, mais je ne sais pas laquelle vous souhaitez.
‡ À titre de comparaison. S'il est utilisé sur une chaîne, le SpookyHash sur 64 bits est considérablement plus rapide que string.GetHashCode()
sur 32 bits, ce qui est légèrement plus rapide que string.GetHashCode()
sur 64 bits, ce qui est considérablement plus rapide que SpookyHash sur 32 bits, bien que suffisamment rapide pour être un choix raisonnable.
GetHashCode
. J'espère que ce serait utile pour les autres. Lignes directrices et règles pour GetHashCode écrites par Eric Lippert