W jaki sposób GetHashCode() w C# string jest zaimplementowany?

Jestem po prostu ciekawa, bo myślę, że będzie to miało wpływ na wydajność. Czy bierze pod uwagę cały ciąg? Jeśli tak, to będzie powolny na długim sznurku. Jeśli bierze pod uwagę tylko część łańcucha, będzie miał złą wydajność (np. jeśli bierze pod uwagę tylko początek łańcucha, będzie miał złą wydajność, jeśli HashSet zawiera głównie Ciągi z tym samym.

Author: Louis Rhys, 2013-03-02

2 answers

Pamiętaj, aby uzyskać Kod źródłowy odniesienia , Gdy masz takie pytania. Jest w tym o wiele więcej niż to, co widać z dekompilatora. Wybierz ten, który pasuje do preferowanego celu. NET, metoda zmieniła wiele między wersjami. Odtworzę tu wersję. NET 4.5, pobraną z 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);
                }
            } 
        }

To prawdopodobnie więcej, niż się spodziewałeś, opiszę kod trochę:

  • dyrektywy kompilacji warunkowej #if dostosowują ten kod do różnych celów. NET. Identyfikatory FEATURE_XX są definiowane gdzie indziej i wyłączają funkcje z całej sprzedaży w kodzie źródłowym. NET. WIN32 jest definiowany, gdy celem jest 32-bitowa wersja frameworka, 64-bitowa wersja mscorlib.biblioteka dll jest budowana oddzielnie i przechowywana w innym podkatalogu GAC.
  • zmienna s_userandomizedstringhashing umożliwia bezpieczną wersję algorytm haszujący, zaprojektowany, aby trzymać programistów z dala od kłopotów, którzy robią coś niemądrego, jak używanie GetHashCode() do generowania hashów dla takich rzeczy, jak hasła lub szyfrowanie. Jest on włączony przez wpis w aplikacji.exe.plik konfiguracyjny
  • stała Instrukcja utrzymuje indeksowanie ciągu, unika sprawdzania granic przez zwykły indekser
  • pierwsze twierdzenie zapewnia, że łańcuch jest zakończony zerem tak, jak powinien być, wymagany do umożliwienia optymalizacji w loop
  • second Assert zapewnia, że łańcuch znaków jest wyrównany do adresu, który jest wielokrotnością 4, tak jak powinien być, wymaganą do zachowania wykonującego pętlę
  • pętla jest rozwijana ręcznie, zużywając 4 znaki na pętlę dla wersji 32-bitowej. Cast to int* to sztuczka polegająca na zapisaniu 2 znaków (2 x 16 bitów) w int (32-bitach). Dodatkowe polecenia po pętli dotyczą ciągu, którego długość nie jest wielokrotnością 4. Należy pamiętać, że Terminator zerowy może lub nie może być zawarty w hash, nie będzie, jeśli długość będzie równa. Wygląda na wszystkie znaki w łańcuchu, odpowiadając na twoje pytanie
  • 64-bitowa wersja pętli jest wykonywana inaczej, ręcznie rozwijana przez 2. Zauważ, że kończy się wcześnie na osadzonym zerze, więc nie patrzy na wszystkie znaki. Poza tym bardzo rzadkie. To dość dziwne, mogę się tylko domyślać, że ma to coś wspólnego z strunami potencjalnie bardzo dużymi. Ale nie mogę wymyślić praktycznego przykładu
  • kod debugowania na koniec zapewnia, że żaden kod w frameworku nigdy nie będzie uzależniony od tego, czy kod hash jest powtarzalny między uruchomieniami.
  • algorytm hash jest dość standardowy. Wartość 1566083941 jest liczbą magiczną, pierwszą, która jest powszechna w Mersenne twister .
 82
Author: Hans Passant,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-09-14 15:24:34

Badając kod źródłowy (dzięki uprzejmości ILSpy ), możemy zobaczyć, że iteracja trwa na całej długości łańcucha.

// 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
Author: Ergwun,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-03-02 12:48:03