web-dev-qa-db-fra.com

Comment GetHashCode () de la chaîne C # est-il implémenté?

Je suis simplement curieux car je suppose que cela aura un impact sur les performances. Considère-t-il la chaîne complète? Si oui, il sera lent sur une longue chaîne. S'il ne considère qu'une partie de la chaîne, il aura de mauvaises performances (par exemple, s'il ne considère que le début de la chaîne, il aura de mauvaises performances si un HashSet contient principalement des chaînes avec les mêmes.

56
Louis Rhys

Assurez-vous d'obtenir le code source de la source de référence lorsque vous avez des questions comme celle-ci. Il y a beaucoup plus que ce que vous pouvez voir avec un décompilateur. Choisissez celle qui correspond à votre cible .NET préférée, la méthode a beaucoup changé entre les versions. Je vais juste en reproduire la version .NET 4.5 ici, récupérée à partir de Source.NET 4.5\4.6.0.0\net\clr\src\BCL\System\String.cs\604718\String.cs

        public override int GetHashCode() { 

#if FEATURE_RANDOMIZED_STRING_HASHING
            if(HashHelpers.s_UseRandomizedStringHashing)
            { 
                return InternalMarvin32HashString(this, this.Length, 0);
            } 
#endif // FEATURE_RANDOMIZED_STRING_HASHING 

            unsafe { 
                fixed (char *src = this) {
                    Contract.Assert(src[this.Length] == '\0', "src[this.Length] == '\\0'");
                    Contract.Assert( ((int)src)%4 == 0, "Managed string should start at 4 bytes boundary");

#if WIN32
                    int hash1 = (5381<<16) + 5381; 
#else 
                    int hash1 = 5381;
#endif 
                    int hash2 = hash1;

#if WIN32
                    // 32 bit machines. 
                    int* pint = (int *)src;
                    int len = this.Length; 
                    while (len > 2) 
                    {
                        hash1 = ((hash1 << 5) + hash1 + (hash1 >> 27)) ^ pint[0]; 
                        hash2 = ((hash2 << 5) + hash2 + (hash2 >> 27)) ^ pint[1];
                        pint += 2;
                        len  -= 4;
                    } 

                    if (len > 0) 
                    { 
                        hash1 = ((hash1 << 5) + hash1 + (hash1 >> 27)) ^ pint[0];
                    } 
#else
                    int     c;
                    char *s = src;
                    while ((c = s[0]) != 0) { 
                        hash1 = ((hash1 << 5) + hash1) ^ c;
                        c = s[1]; 
                        if (c == 0) 
                            break;
                        hash2 = ((hash2 << 5) + hash2) ^ c; 
                        s += 2;
                    }
#endif
#if DEBUG 
                    // We want to ensure we can change our hash function daily.
                    // This is perfectly fine as long as you don't persist the 
                    // value from GetHashCode to disk or count on String A 
                    // hashing before string B.  Those are bugs in your code.
                    hash1 ^= ThisAssembly.DailyBuildNumber; 
#endif
                    return hash1 + (hash2 * 1566083941);
                }
            } 
        }

C'est peut-être plus que ce que vous aviez prévu, je vais annoter un peu le code:

  • Les directives de compilation conditionnelle #if adaptent ce code à différentes cibles .NET. Les identifiants FEATURE_XX sont définis ailleurs et désactivent toutes les fonctionnalités dans le code source .NET. WIN32 est défini lorsque la cible est la version 32 bits du framework, la version 64 bits de mscorlib.dll est générée séparément et stockée dans un sous-répertoire différent du GAC.
  • La variable s_UseRandomizedStringHashing permet une version sécurisée de l'algorithme de hachage, conçue pour garder les programmeurs à l'abri de problèmes qui font quelque chose d'imprudent comme utiliser GetHashCode () pour générer des hachages pour des choses comme les mots de passe ou le chiffrement. Il est activé par ne entrée dans le fichier app.exe.config
  • L'instruction fixe garde l'indexation de la chaîne bon marché, évite la vérification des limites effectuée par l'indexeur normal
  • La première assertion garantit que la chaîne se termine par un zéro comme il se doit, nécessaire pour permettre l'optimisation dans la boucle
  • La deuxième assertion garantit que la chaîne est alignée sur une adresse qui est un multiple de 4 comme il se doit, nécessaire pour maintenir la boucle performante
  • La boucle est déroulée à la main, consommant 4 caractères par boucle pour la version 32 bits. Le transtypage en int * est une astuce pour stocker 2 caractères (2 x 16 bits) dans un int (32 bits). Les instructions supplémentaires après la boucle traitent d'une chaîne dont la longueur n'est pas un multiple de 4. Notez que le terminateur zéro peut ou non être inclus dans le hachage, il ne le sera pas si la longueur est paire. Il regarde tous les caractères de la chaîne, répondant à votre question
  • La version 64 bits de la boucle se fait différemment, déroulée manuellement par 2. Notez qu'elle se termine tôt sur un zéro intégré, donc ne regarde pas tous les caractères. Sinon, très rare. C'est assez étrange, je peux seulement deviner que cela a quelque chose à voir avec des chaînes potentiellement très grandes. Mais je ne peux pas penser à un exemple pratique
  • Le code de débogage à la fin garantit qu'aucun code dans le cadre ne prend jamais une dépendance sur le code de hachage reproductible entre les exécutions.
  • L'algorithme de hachage est assez standard. La valeur 1566083941 est un nombre magique, un nombre premier commun dans un twister de Mersenne .
89
Hans Passant

En examinant le code source (avec la permission de ILSpy ), nous pouvons voir qu'il itère sur la longueur de la chaîne.

// string
[ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail), SecuritySafeCritical]
public unsafe override int GetHashCode()
{
    IntPtr arg_0F_0;
    IntPtr expr_06 = arg_0F_0 = this;
    if (expr_06 != 0)
    {
        arg_0F_0 = (IntPtr)((int)expr_06 + RuntimeHelpers.OffsetToStringData);
    }
    char* ptr = arg_0F_0;
    int num = 352654597;
    int num2 = num;
    int* ptr2 = (int*)ptr;
    for (int i = this.Length; i > 0; i -= 4)
    {
        num = ((num << 5) + num + (num >> 27) ^ *ptr2);
        if (i <= 2)
        {
            break;
        }
        num2 = ((num2 << 5) + num2 + (num2 >> 27) ^ ptr2[(IntPtr)4 / 4]);
        ptr2 += (IntPtr)8 / 4;
    }
    return num + num2 * 1566083941;
}
6
Ergwun