Dans .NET, la GetHashCode
méthode est utilisée à de nombreux endroits dans les bibliothèques de classes de base .NET. L'implémenter correctement est particulièrement important pour trouver rapidement des éléments dans une collection ou pour déterminer l'égalité.
Existe-t-il un algorithme standard ou une meilleure pratique sur la façon d'implémenter GetHashCode
pour mes classes personnalisées afin de ne pas dégrader les performances?
.net
algorithm
hashcode
gethashcode
bitbonk
la source
la source
GetHashCode
. J'espère que ce serait utile pour les autres. Lignes directrices et règles pour GetHashCode écrites par Eric LippertGetHashCode()
est toujours utilisé dans de très nombreuses implémentations deEquals()
. C'est ce que je voulais dire avec cette déclaration.GetHashCode()
insideEquals()
est souvent utilisé comme raccourci pour déterminer l' inégalité , car si deux objets ont un code de hachage différent, ils doivent être des objets qui ne sont pas égaux et le reste du contrôle d'égalité n'a pas à être exécuté.GetHashCode()
etEquals()
doivent regarder tous les champs des deux objets (Equals doit le faire si les codes de hachage sont égaux ou non vérifiés). Pour cette raison, un appel vers l'GetHashCode()
intérieurEquals()
est souvent redondant et pourrait réduire les performances.Equals()
peut également être en mesure de court-circuiter, ce qui le rend beaucoup plus rapide - cependant dans certains cas, les codes de hachage peuvent être mis en cache, ce qui rend laGetHashCode()
vérification plus rapide et donc utile. Voir cette question pour plus.Réponses:
J'utilise généralement quelque chose comme l'implémentation donnée dans le fabuleux Java efficace de Josh Bloch . Il est rapide et crée un très bon hachage qui est peu susceptible de provoquer des collisions. Choisissez deux nombres premiers différents, par exemple 17 et 23, et faites:
Comme indiqué dans les commentaires, vous trouverez peut-être préférable de choisir un grand nombre premier à multiplier par. Apparemment, 486187739 est bon ... et bien que la plupart des exemples que j'ai vus avec de petits nombres aient tendance à utiliser des nombres premiers, il existe au moins des algorithmes similaires où des nombres non premiers sont souvent utilisés. Dans l' exemple FNV pas tout à fait plus tard, par exemple, j'ai utilisé des nombres qui semblent bien fonctionner - mais la valeur initiale n'est pas un nombre premier. (La constante de multiplication est cependant primordiale. Je ne sais pas à quel point c'est important.)
C'est mieux que la pratique courante d'
XOR
ingérer des codes de hachage pour deux raisons principales. Supposons que nous ayons un type avec deuxint
champs:Soit dit en passant, l'algorithme précédent est celui actuellement utilisé par le compilateur C # pour les types anonymes.
Cette page propose plusieurs options. Je pense que dans la plupart des cas, ce qui précède est "assez bon" et c'est incroyablement facile à retenir et à bien faire. L' alternative FNV est tout aussi simple, mais utilise des constantes différentes et
XOR
nonADD
comme une opération de combinaison. Il ressemble quelque chose comme le code ci - dessous, mais l'algorithme de FNV normale fonctionne sur des octets individuels, donc cela nécessiterait la modification d'effectuer une itération par octet, au lieu de par la valeur de hachage 32 bits. FNV est également conçu pour des longueurs de données variables, alors que la façon dont nous les utilisons ici est toujours pour le même nombre de valeurs de champ. Les commentaires sur cette réponse suggèrent que le code ici ne fonctionne pas aussi bien (dans l'exemple de cas testé) que l'approche d'addition ci-dessus.Notez qu'une chose à savoir est que, idéalement, vous devriez empêcher votre état sensible à l'égalité (et donc sensible au code de hachage) de changer après l'avoir ajouté à une collection qui dépend du code de hachage.
Selon la documentation :
la source
Dictionary<TKey,TValue>
suppose une bonne distribution modulo certains nombres premiers. Et 23 est l'un d'entre eux. Donc, si vous avez un dictionnaire avec Capacity 23, seule la dernière contribution àGetHashCode
influence le code de hachage composé. Je préfère donc utiliser 29 au lieu de 23.null
- ce qui n'est pas la même chose que d'ignorer le champ.Type anonyme
Microsoft fournit déjà un bon générateur générique HashCode: copiez simplement vos valeurs de propriété / champ dans un type anonyme et hachez-le:
Cela fonctionnera pour n'importe quel nombre de propriétés. Il n'utilise pas de boxe. Il utilise simplement l'algorithme déjà implémenté dans le cadre pour les types anonymes.
ValueTuple - Mise à jour pour C # 7
Comme @cactuaroid le mentionne dans les commentaires, un tuple de valeur peut être utilisé. Cela permet d'économiser quelques frappes et, plus important encore, de s'exécuter uniquement sur la pile (pas de déchets):
(Remarque: la technique d'origine utilisant des types anonymes semble créer un objet sur le tas, c'est-à-dire des ordures, car les types anonymes sont implémentés en tant que classes, bien que cela puisse être optimisé par le compilateur. Il serait intéressant de comparer ces options, mais le l'option tuple doit être supérieure.)
la source
GetHashCode
implémentation anonyme est très efficace (BTW c'est la même que celle de la réponse de Jon Skeet), mais le seul problème avec cette solution est que vous générez une nouvelle instance à chaqueGetHashCode
appel. Cela peut être un peunew { PropA, PropB, PropC, PropD }.GetHashCode()
tropNew With {Key PropA}.GetHashCode()
sinon GetHashCode ne renverra pas le même code de hachage pour différents objets avec les mêmes propriétés «d'identification».Voici mon assistant de hachage.
Son avantage est qu'il utilise des arguments de type générique et ne causera donc pas de boxe:
Il a également une méthode d'extension pour fournir une interface fluide, vous pouvez donc l'utiliser comme ceci:
ou comme ça:
la source
T[]
séparément car il est déjàIEnumerable<T>
J'ai une classe de hachage dans la bibliothèque d'aide que je l'utilise à cet effet.
Ensuite, vous pouvez simplement l'utiliser comme:
Je n'ai pas évalué ses performances, tout commentaire est donc le bienvenu.
la source
unchecked
méthode est d'éviter les exceptions de débordement souhaitéesGetHashCode
. Ce n'est donc pas incorrect si la valeur débordeint
et que cela ne fait pas de mal du tout.null
être entièrement ignoré pourrait vous donner des résultats inattendus. Au lieu de les ignorer, vous devez simplement utiliser une valeur constante au lieu deinput[i].GetHashCode()
quandinput[i]
est nul.Voici ma classe d'aide utilisant l'implémentation de Jon Skeet .
Usage:
Si vous souhaitez éviter d'écrire une méthode d'extension pour System.Int32:
Il évite toujours toute allocation de tas et est utilisé exactement de la même manière:
Edit (mai 2018):
EqualityComparer<T>.Default
getter est maintenant un intrinsèque JIT - la demande de pull est mentionnée par Stephen Toub dans ce billet de blog .la source
var h = Equals(obj, default(T)) ? 0 : obj.GetHashCode();
obj != null
compilera unebox
instruction qui allouera de la mémoire siT
est un type de valeur. À la place, vous pouvez utiliserobj.Equals(null)
ce qui se compilera en un appel virtuel de laEquals
méthode.this.hashCode != h
. Il ne retournerait pas la même valeur..NET Standard 2.1 et supérieur
Si vous utilisez .NET Standard 2.1 ou supérieur, vous pouvez utiliser la structure System.HashCode . Il existe deux méthodes pour l'utiliser:
HashCode.Combine
La
Combine
méthode peut être utilisée pour créer un code de hachage, donné jusqu'à huit objets.HashCode.Add
La
Add
méthode vous aide à gérer les collections:GetHashCode Made Easy
Vous pouvez lire le billet de blog complet « GetHashCode Made Easy » pour plus de détails et de commentaires.
Exemple d'utilisation
la mise en oeuvre
Qu'est-ce qui fait un bon algorithme?
La vitesse
L'algorithme qui calcule un code de hachage doit être rapide. Un algorithme simple va généralement être plus rapide.
Déterministe
L'algorithme de hachage doit être déterministe, c'est-à-dire que pour la même entrée, il doit toujours produire la même sortie.
Réduisez les collisions
L'algorithme qui calcule un code de hachage doit conserver les collisions de hachage à un minimum. Une collision de hachage est une situation qui se produit lorsque deux appels à
GetHashCode
deux objets différents produisent des codes de hachage identiques. Notez que les collisions sont autorisées (certains pensent à tort qu'elles ne le sont pas) mais elles doivent être réduites au minimum.Une bonne fonction de hachage doit mapper les entrées attendues aussi uniformément que possible sur sa plage de sortie. Il devrait avoir une uniformité.
Prevent's DoS
Dans .NET Core, chaque fois que vous redémarrez une application, vous obtenez différents codes de hachage. Il s'agit d'une fonction de sécurité pour empêcher les attaques par déni de service (DoS). Pour .NET Framework , vous devez activer cette fonctionnalité en ajoutant le fichier App.config suivant:
En raison de cette fonctionnalité, les codes de hachage ne doivent jamais être utilisés en dehors du domaine d'application dans lequel ils ont été créés, ils ne doivent jamais être utilisés comme champs clés dans une collection et ils ne doivent jamais être persistants.
En savoir plus à ce sujet ici .
Cryptographiquement sécurisé?
Il n'est pas nécessaire que l'algorithme soit une fonction de hachage cryptographique . Cela signifie qu'il ne doit pas satisfaire aux conditions suivantes:
la source
Dans la plupart des cas où Equals () compare plusieurs champs, peu importe que votre GetHash () hache sur un champ ou sur plusieurs. Vous devez juste vous assurer que le calcul du hachage est vraiment bon marché ( pas d'allocations , s'il vous plaît) et rapide ( pas de calculs lourds et certainement pas de connexions à la base de données) et fournit une bonne distribution.
Le levage de charges lourdes doit faire partie de la méthode Equals (); le hachage devrait être une opération très bon marché pour permettre d'appeler Equals () sur le moins d'éléments possible.
Et une dernière astuce: ne vous fiez pas à la stabilité de GetHashCode () sur plusieurs exécutions d'applications . De nombreux types .Net ne garantissent pas que leurs codes de hachage restent identiques après un redémarrage, vous ne devez donc utiliser que la valeur de GetHashCode () pour les structures de données en mémoire.
la source
GetHashCode
effectuer des allocations de mémoire, à condition qu'il ne le fasse que la première fois qu'il est utilisé (avec des invocations ultérieures renvoyant simplement un résultat mis en cache). L'important n'est pas de se donner beaucoup de mal pour éviter les collisions, mais plutôt d'éviter les collisions "systémiques". Si un type a deuxint
champsoldX
etnewX
qui diffèrent fréquemment d'un, une valeur de hachageoldX^newX
affecterait 90% de ces enregistrements à des valeurs de hachage de 1, 2, 4 ou 8. L'utilisation deoldX+newX
[l'arithmétique non vérifiée] pourrait générer plus de collisions ...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:
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:
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:
* 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 propreGetHashCode()
considère la précision comme significative tandis que le sienEquals()
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 questring.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.la source
long
valeurs pour les résultats intermédiaires, puis à fusionner le résultat final jusqu'à unint
. Cela vous semble-t-il une bonne idée? Ma préoccupation est que l'on utilise par exemple hash = (hash * 31) + nextField, alors les paires de valeurs correspondantes n'affecteront que les 27 bits supérieurs du hachage. Laisser le calcul s'étendre à unlong
et emballer des choses minimiserait ce danger..Update()
avec les valeurs multiples selon la réponse ci-dessus fera l'affaire.C'est une bonne:
Et voici comment l'utiliser:
la source
GetHashCode()
méthode, vous pouvez donc toujours utiliser la méthode avec leparams
paramètre tableau. Ou est-ce que je manque quelque chose ici?h += (h << 10); h ^= (h >> 6); h += (h << 3); h ^= (h >> 11); h += (h << 15);
ont un codemell: elles ne dépendent d'aucune entrée et me semblent terriblement redondantes.Depuis https://github.com/dotnet/coreclr/pull/14863 , il existe une nouvelle façon de générer des codes de hachage qui est super simple! Ecrivez
Cela générera un code de hachage de qualité sans que vous ayez à vous soucier des détails de mise en œuvre.
la source
HashCode
changements pour corefx ont été fusionnés quelques heures avant votre commentaire :) Le type devrait être livré dans .NET Core 2.1.Voici une autre implémentation fluide de l'algorithme publié ci-dessus par Jon Skeet , mais qui ne comprend aucune allocation ou opération de boxe:
Usage:
Le compilateur s'assurera qu'il
HashValue
n'est pas appelé avec une classe en raison de la contrainte de type générique. Mais il n'y a pas de prise en charge du compilateurHashObject
car l'ajout d'un argument générique ajoute également une opération de boxe.la source
Voici mon approche simpliste. J'utilise le modèle de générateur classique pour cela. Il est de type sécurisé (pas de boxe / unboxing) et également compatible avec .NET 2.0 (pas de méthodes d'extension, etc.).
Il est utilisé comme ceci:
Et voici la classe de constructeur acutal:
la source
AddItems<T>(params T[] items)
méthode plus souvent dans la classe d'assistance (que d'appeler àAddItem(T)
chaque fois).this.result * Prime2 * item.GetHashCode()
souventthis.result * Prime2 + item.GetHashCode()
?AddItems<T>(params T[] items)
plus souvent parce quetypeof(T1) != typeof(T2)
etc.Les utilisateurs de ReSharper peuvent générer GetHashCode, Equals et autres avec
ReSharper -> Edit -> Generate Code -> Equality Members
.la source
Si nous n'avons pas plus de 8 propriétés (espérons-le), voici une autre alternative.
ValueTuple
est une structure et semble avoir uneGetHashCode
implémentation solide .Cela signifie que nous pourrions simplement faire ceci:
Jetons un coup d' oeil à la mise en œuvre actuelle de .NET de base pour
ValueTuple
« sGetHashCode
.Cela vient de
ValueTuple
:Et cela vient de
HashHelper
:En anglais:
Ce serait bien d'en savoir plus sur les propriétés de cet algorithme de code de hachage ROL-5.
Malheureusement, le report de la
ValueTuple
nôtreGetHashCode
ne sera peut-être pas aussi rapide que nous le souhaiterions. Ce commentaire dans une discussion connexe illustre que l'appel directHashHelpers.Combine
est plus performant. D'un autre côté, celui-ci est interne, il nous faudrait donc copier le code, sacrifiant une grande partie de ce que nous avions gagné ici. De plus, nous serions responsables de nous rappeler d'abordCombine
avec la graine aléatoire. Je ne sais pas quelles sont les conséquences si nous sautons cette étape.la source
h1 >> 27
0 l'ignore,h1 << 5
est égal àh1 * 32
donc c'est la même chose queh1 * 33 ^ h2
. Selon cette page , il s'appelle "Bernstein modifié".La plupart de mon travail se fait avec la connectivité à la base de données, ce qui signifie que mes classes ont toutes un identifiant unique de la base de données. J'utilise toujours l'ID de la base de données pour générer le code de hachage.
la source
_id.GetHashCode
car l'intention est claire.Assez similaire à la solution de Nightcoder, sauf qu'il est plus facile d'augmenter les nombres premiers si vous le souhaitez.
PS: C'est l'un de ces moments où vous vomissez un peu dans votre bouche, sachant que cela pourrait être refactorisé en une seule méthode avec 9 valeurs par défaut, mais ce serait plus lent, alors fermez les yeux et essayez de l'oublier.
la source
J'ai rencontré un problème avec les flottants et les décimales en utilisant l'implémentation sélectionnée comme réponse ci-dessus.
Ce test échoue (flotte; le hachage est le même même si j'ai changé 2 valeurs pour être négatif):
Mais ce test réussit (avec des pouces):
J'ai changé mon implémentation pour ne pas utiliser GetHashCode pour les types primitifs et cela semble mieux fonctionner
la source
unchecked
n'affecte pasConvert.ToInt32
:uint
,long
,float
,double
etdecimal
peuvent tous déborder ici.Microsoft mène plusieurs hachages ...
Je peux deviner que pour plusieurs gros int, vous pouvez utiliser ceci:
Et même pour multi-type: tous convertis d'abord en
int
utilisantGetHashCode()
ensuite les valeurs int seront xor'ed et le résultat est votre hachage.Pour ceux qui utilisent le hachage comme ID (je veux dire une valeur unique), le hachage est naturellement limité à un certain nombre de chiffres, je pense que c'était 5 octets pour l'algorithme de hachage, au moins MD5.
Vous pouvez transformer plusieurs valeurs en une valeur hachée et certaines d'entre elles doivent être identiques, alors ne l'utilisez pas comme identifiant. (peut-être qu'un jour je vais utiliser votre composant)
la source
Il s'agit d'une classe d'assistance statique qui implémente l'implémentation de Josh Bloch; et fournit des surcharges explicites pour "empêcher" la boxe, et également pour implémenter le hachage spécifiquement pour les primitives longues.
Vous pouvez passer une comparaison de chaînes qui correspond à votre implémentation égale.
Comme la sortie Hash est toujours un entier, vous pouvez simplement enchaîner les appels Hash.
la source
HashKeysAndValues
méthode a été corrigée: elle invoqueHashKeyAndValue
.Si vous souhaitez effectuer un polyfill à
HashCode
partir denetstandard2.1
Remarque: s'il est utilisé avec
struct
, il allouera de la mémoire en raison de la boxela source