2010-10-01 24 views
20

De ValueType.csPourquoi ValueType.GetHashCode() est-il implémenté comme il est?

 
**Action: Our algorithm for returning the hashcode is a little bit complex. We look 
**  for the first non-static field and get it's hashcode. If the type has no 
**  non-static fields, we return the hashcode of the type. We can't take the 
**  hashcode of a static member because if that member is of the same type as 
**  the original type, we'll end up in an infinite loop. 

Je me suis mordu par aujourd'hui quand je travaillais avec un KeyValuePair comme une clé dans un dictionnaire (il stocké nom d'attribut xml (enum) et sa valeur (string)), et prévu pour elle pour avoir son hash code calculé sur la base de tous ses champs, mais selon l'implémentation, il ne considère que la partie clé.

Exemple (c/p de LINQPad):

void Main() 
{ 
    var kvp1 = new KeyValuePair<string, string>("foo", "bar"); 
    var kvp2 = new KeyValuePair<string, string>("foo", "baz"); 

    // true 
    (kvp1.GetHashCode() == kvp2.GetHashCode()).Dump(); 
} 

Le premier champ non statique Je suppose que signifie le premier champ afin de declaratin, qui pourrait aussi causer des problèmes lors du changement de commande variable dans la source pour une raison quelconque , et croyant qu'il ne change pas le code sémantiquement.

Répondre

31

Je ne l'ai pas implémenté et je n'ai pas parlé à ceux qui l'ont fait. Mais je peux signaler quelques choses.

(Avant de continuer, notez que je parle ici spécifiquement des codes de hachage pour l'équilibrage des tables de hachage où le contenu de la table est choisi par des utilisateurs non hostiles. la vérification de la redondance ou la garantie d'une bonne performance d'une table de hachage lorsque certains utilisateurs montent des attaques par déni de service contre le fournisseur de tables dépassent le cadre de cette discussion.)

L'algorithme met en œuvre le contrat requis de GetHashCode. Cela pourrait être sous-optimal pour vos objectifs, mais c'est légal. Tout ce qui est requis est que les choses qui comparent égal ont des codes de hachage égaux.

Alors, quels sont les «bons à avoir» en plus de ce contrat? Une bonne implémentation du code de hachage doit être:

1) Rapide. Très vite! Rappelez-vous, tout le point du code de hachage en premier lieu à rapidement trouver un emplacement relativement vide dans une table de hachage. Si le calcul O (1) du code de hachage est en pratique plus lent que le temps O (n) pris pour effectuer la recherche naïvement alors la solution de code de hachage est une perte nette.

2) Bien répartie sur l'espace des entiers de 32 bits pour la distribution donnée des entrées. Plus la distribution est mauvaise, plus la recherche linéaire naïve de la table de hachage sera bonne.Alors, comment feriez-vous un algorithme de hachage pour les types de valeurs arbitraires étant donné que ces deux conflits de objectifs? Chaque fois que vous dépensez sur un algorithme de hachage complexe qui garantit une bonne distribution, le temps est mal dépensé.

Une suggestion courante consiste à "hacher tous les champs et ensuite XOR ensemble les codes de hachage résultants". Mais c'est implorer la question; XOR deux 32 bits ints ne donne une bonne distribution lorsque les entrées elles-mêmes sont très bien distribués et non liés les uns aux autres, et qui est un scénario improbable:

// (Updated example based on good comment!) 
struct Control 
{ 
    string name; 
    int x; 
    int y; 
} 

Quelle est la probabilité que x et y sont bien distribué sur toute la gamme des entiers 32 bits? Très lent. Les chances sont beaucoup mieux qu'ils sont à la fois petit et près de l'autre, auquel cas XOR leurs codes de hachage rend les choses ensemble pire, pas mieux. La coexistence d'entiers proches les uns des autres met à zéro la plupart des bits.

De plus, c'est O (n) dans le nombre de champs! Un type de valeur avec beaucoup de petits champs prendrait un temps relativement long pour calculer le code de hachage.

Fondamentalement, la situation dans laquelle nous sommes ici est que l'utilisateur n'a pas fourni lui-même une implémentation de code de hachage; soit ils s'en fichent, soit ils ne s'attendent pas à ce que ce type soit utilisé comme clé dans une table de hachage. Étant donné que vous avez aucune information sémantique sur le type, quelle est la meilleure chose à faire? La meilleure chose à faire est ce qui est rapide et donne de bons résultats la plupart du temps.

La plupart du temps, deux instances de struct qui diffèrent diffèrent en plus de leurs champs, et pas seulement un de leurs champs, de sorte que la cueillette un d'entre eux et en espérant que c'est celui qui est différent semble raisonnable. La plupart du temps, deux instances struct qui diffèrent auront une certaine redondance dans leurs champs, donc combiner les valeurs de hachage de plusieurs champs ensemble est susceptible de diminuer, et non d'augmenter, l'entropie dans la valeur de hachage, même si elle consomme le temps que l'algorithme de hachage est conçu pour enregistrer.

Comparez cela avec la conception de types anonymes en C#. Avec les types anonymes, nous savons qu'il est fort probable que le type soit utilisé comme clé pour une table. Nous savons qu'il est très probable qu'il y aura une redondance entre les instances de types anonymes (parce qu'elles sont le résultat d'un produit cartésien ou d'une autre jointure). Et donc, nous combinons les codes de hachage de tous les champs en un seul code de hachage. Si cela vous donne de mauvaises performances en raison du nombre excessif de codes de hachage calculés, vous êtes libre d'utiliser un type nominal personnalisé plutôt que le type anonyme.

+0

Votre exemple de Point n'était pas bon. Vous obtiendrez un «bon» hash de celui-là. J'ai laissé un post pour décrire pourquoi c'est différent. –

+1

En supposant que vous ne faites pas juste un XOR, la combinaison de plusieurs hashcodes corrélés ne diminuera généralement pas l'entropie - et il n'y a pas de raison particulièrement pressante de faire un XOR. –

+0

@EamonNerbonne: Le seul avantage que je peux voir pour XOR est que dans les langages qui n'ont aucun moyen de demander une arithmétique signée "non vérifiée", il permet la gamme complète des valeurs de sortie sans masquage manuel. Sinon, c'est dans tous les sens que je peux voir inférieur à '+ ​​'. L'addition est aussi rapide que xor dans les langages qui peuvent supporter une arithmétique signée non vérifiée, et fonctionne correctement dans les cas courants où deux champs sont toujours égaux ou diffèrent par une constante. – supercat

7

Il devrait toujours obéir au contrat de GetHashCode même si l'ordre des champs change: des valeurs égales auront des codes de hachage égaux, pendant la durée de vie de ce processus.

En particulier:

  • valeurs non égales ne doivent pas avoir des codes de hachage non égales
  • codes de hachage ne doivent pas être cohérents dans tous les processus (vous pouvez changer une implémentation, reconstruisez , et tout devrait fonctionner - vous ne devriez pas être persistant codes de hachage, essentiellement)

maintenant, je ne dis pas que la mise en œuvre de ValueType est une excellente idée - elle causera suckage performance de diverses manières ... mais Je ne pense pas que ce soit en fait cassé.

3

Eh bien, il existe des avantages et des inconvénients à toute mise en œuvre de GetHashCode(). Ce sont bien sûr les choses que nous pesons lorsque nous mettons en œuvre les nôtres, mais dans le cas de ValueType.GetHashCode(), il y a une difficulté particulière en ce sens qu'ils n'ont pas beaucoup d'informations sur ce que seront les détails concrets du type concret. Bien sûr, cela nous arrive souvent quand nous créons une classe abstraite ou une classe destinée à être la base de classes qui ajouteront beaucoup plus en termes d'état, mais dans ces cas nous avons une solution évidente de simplement utiliser l'implémentation par défaut. de object.GetHashCode() sauf si une classe dérivée se soucie de le remplacer là. Avec ValueType.GetHashCode() avec ValueType.GetHashCode() ils n'ont pas ce luxe car la principale différence entre un type de valeur et un type de référence est, malgré la popularité de parler des détails d'implémentation de la pile par rapport au tas, le fait que pour une équivalence de type valeur à la valeur alors que pour une équivalence type d'objet se rapporte à l'identité (même lorsqu'un objet définit une autre forme d'équivalence en remplaçant Equals() et GetHashCode() le concept de référence l'égalité existe toujours et est encore utile.

Ainsi, pour la méthode Equals() l'implémentation est évidente, vérifiez que les deux objets sont du même type, et si c'est le cas, vérifiez aussi que tous les champs sont égaux (en fait, il y a un optimisa tion qui fait une comparaison des bits dans certains cas, mais c'est une optimisation sur la même idée de base).

Que faire pour GetHashCode()? Il n'y a simplement pas de solution parfaite. Une chose qu'ils pourraient faire est une sorte de mult-then-add ou shift-then-xor sur chaque champ.Cela donnerait probablement un bon hash-code, mais pourrait être coûteux s'il y avait beaucoup de champs (peu importe qu'il ne soit pas recommandé d'avoir des types de valeur qui ont beaucoup de champs, l'implémenteur doit considérer qu'ils peuvent encore, et en effet il peut même y avoir des moments où cela a du sens, bien que je ne puisse honnêtement imaginer un moment où cela soit à la fois sensé et sensé de le faire). S'ils savaient que certains champs étaient rarement différents entre les instances, ils pouvaient ignorer ces champs et toujours avoir un bon code, tout en étant assez rapide. Enfin, ils peuvent ignorer la plupart des champs et espérer que ceux qu'ils n'ignorent pas varient en valeur la plupart du temps. Ils sont allés pour la version la plus extrême de ce dernier. (La question de savoir ce qui est fait quand il n'y a pas de champs d'instance est une autre affaire et un très bon choix, ces types de valeur sont égaux à toutes les autres instances du même type, et ils ont un hashcode qui correspond à cela) . Donc, c'est une implémentation qui aspire si vous êtes hashing beaucoup de valeurs où le premier champ est le même (ou retourne le même hashcode), mais d'autres implémentations seraient nulles dans d'autres cas (Mono va pour xoring tous les champs 'hashcodes ensemble, mieux dans votre cas, pire dans d'autres). La question de changer l'ordre des champs n'a pas d'importance, car le hashcode est clairement indiqué comme étant seulement valable pour la durée de vie d'un processus et ne convenant pas dans la plupart des cas où il pourrait être conservé au-delà (cela peut être utile). certaines situations de mise en cache où il ne fait pas de mal si les choses ne sont pas trouvées correctement après un changement de code). Donc, pas génial, mais rien ne serait parfait. Cela montre qu'il faut toujours considérer les deux côtés de ce que «l'égalité» signifie quand on utilise un objet comme clé. Il est facile à régler dans votre cas:

public class KVPCmp<TKey, TValue> : IEqualityComparer<KeyValuePair<TKey, TValue>>, IEqualityComparer 
{ 
    bool IEqualityComparer.Equals(object x, object y) 
    { 
     if(x == null) 
     return y == null; 
     if(y == null) 
     return false; 
     if(!(x is KeyValuePair<TKey, TValue>) || !(y is KeyValuePair<TKey, TValue>)) 
     throw new ArgumentException("Comparison of KeyValuePairs only."); 
     return Equals((KeyValuePair<TKey, TValue>) x, (KeyValuePair<TKey, TValue>) y); 
    } 
    public bool Equals(KeyValuePair<TKey, TValue> x, KeyValuePair<TKey, TValue> y) 
    { 
     return x.Key.Equals(y.Key) && x.Value.Equals(y.Value); 
    } 
    public int GetHashCode(KeyValuePair<TKey, TValue> obj) 
    { 
     int keyHash = obj.GetHashCode(); 
     return ((keyHash << 16) | (keyHash >> 16))^obj.Value.GetHashCode(); 
    } 
    public int GetHashCode(object obj) 
    { 
     if(obj == null) 
     return 0; 
     if(!(obj is KeyValuePair<TKey, TValue>)) 
     throw new ArgumentException(); 
     return GetHashCode((KeyValuePair<TKey, TValue>)obj); 
    } 
} 

Utilisez ce que votre comparateur lors de la création de votre dictionnaire, et tout doit être bien (vous avez seulement besoin des méthodes de comparaison génériques vraiment, mais en laissant le reste en fait pas de mal et peut être utile d'avoir parfois).

39

L'implémentation réelle de ValueType.GetHashCode() ne correspond pas tout à fait au commentaire. Il a deux versions de l'algorithme, rapide et lente. Il vérifie d'abord si la structure contient des membres d'un type de référence et s'il existe un remplissage entre les champs. Le remplissage est un espace vide dans une valeur de structure, créé lorsque le compilateur JIT aligne les champs. Il ya un remplissage dans une structure qui contient bool et int (3 octets) mais pas de remplissage quand il contient int et int, ils s'adaptent parfaitement ensemble.

Sans référence et sans bourrage, il peut faire la version rapide puisque chaque bit dans la valeur de structure est un bit qui appartient à une valeur de champ. Il ne fait que xor 4 octets à la fois. Vous obtiendrez un «bon» code de hachage qui tient compte de tous les membres. De nombreux types de structure simples dans le framework .NET se comportent de cette manière, comme Point and Size.

A défaut de ce test, il fait la version lente, l'équivalent moral de la réflexion. C'est ce que vous obtenez, votre KeyValuePair <> contient des références. Et celui-ci ne vérifie que le premier champ candidat, comme le dit le commentaire. C'est sûrement une optimisation de perf, évitant de brûler trop de temps.

Oui, détail méchant et pas très connu. Il est généralement découvert lorsque quelqu'un remarque que son code de collecte suce la boue. Un autre détail insoutenable: la version rapide a un bug qui octet quand la structure contient un champ d'un type décimal. Les valeurs 12m et 12.0m sont logiquement égales mais n'ont pas le même schéma de bits. GetHashCode() dira qu'ils ne sont pas égaux. Aie.

+0

Bon point, j'avais oublié ce détail. –

+5

Plus généralement, le bogue que vous mentionnez s'applique à tout type de valeur pour lequel l'égalité n'implique pas l'égalité au niveau du bit, dont la décimale n'est qu'un exemple. Vraiment, la version accélérée optimisée ne doit pas être utilisée lorsqu'un membre de la structure remplace les valeurs égales ou gethashcode. Probablement juste ne pas l'utiliser quand un membre est une structure serait mieux (plutôt que de passer plus de temps à voir si l'on peut faire la version rapide, que de faire la sauvegarde rapide de la version). –

+0

@JonHanna: Je dirais que la version dite "fast-track" est la façon dont 'Object.Equals' devrait fonctionner. Si l'on a deux conteneurs immuables dont les éléments correspondants indiquent tous «Equal», remplacer les références à la seconde par des références à la première (ou vice versa) devrait être un moyen sûr d'économiser de la mémoire et d'accélérer les comparaisons futures. Une telle substitution est sans danger pour les types où 'Equals' ne renvoie que true pour les éléments équivalents, mais n'est pas sûr pour les types où les éléments non-équivalents peuvent se déclarer" égaux "les uns aux autres. Le fait que '12m.Equals (12.0m)' ne signifie pas vraiment qu'il devrait. – supercat

0

Merci à tous pour vos réponses très, très instructives. Je savais que cette décision devait être justifiée, mais j'aurais aimé qu'elle soit mieux documentée. Je ne suis pas en mesure d'utiliser v4 du cadre, donc il n'y a pas Tuple<>, et c'était la principale raison pour laquelle j'ai décidé de greffer KeyValuePair struct. Mais je suppose qu'il n'y a pas de raccourcis et je vais devoir rouler le mien. Encore une fois, merci à tous.