Dlaczego jest ValueType.GetHashCode () zaimplementowane tak jak jest?

Z ValueType.cs

**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.

Ugryzło mnie to dzisiaj, kiedy używałem KeyValuePair jako klucza w słowniku (przechowywał nazwę atrybutu xml (enum) i jego wartość (string)) i oczekiwałem, że kod hash będzie obliczany na podstawie wszystkich pól, ale zgodnie z implementacją uwzględniał tylko część klucza.

Przykład (c / P z 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();
}

Pierwsze pole niestatyczne chyba oznacza pierwsze pole w porządku deklaratywnym, które może również powodować kłopoty, gdy Zmiana kolejności zmiennych w źródle z jakiegokolwiek powodu i przekonanie, że nie zmienia to semantycznie kodu.

Author: alh84001, 2010-10-01

5 answers

Nie wdrożyłem tego i nie rozmawiałem z ludźmi, którzy to zrobili. Ale mogę wskazać kilka rzeczy.

(zanim przejdę dalej, zwróć uwagę, że tutaj mówię konkretnie o kodach hashowych do celów balansowania tabel hashowych, gdzie zawartość tabeli jest wybierana przez nie-wrogich użytkowników. Problemy związane z kodami skrótu do podpisywania cyfrowego, sprawdzania nadmiarowości lub zapewnienia dobrej wydajności tabeli skrótu, gdy niektórzy użytkownicy montują ataki typu denial-of-service przeciwko tabeli dostawcy są poza zakresem tej dyskusji.)

Po pierwsze, jak Jon poprawnie zauważa, dany algorytm implementuje wymagany kontrakt GetHashCode. Może to być nieoptymalne dla Twoich celów, ale jest legalne. Wszystko, co jest wymagane , to to, że rzeczy, które porównują równe, mają równe kody hash.

Więc co to są "miłe mieć" oprócz tego kontraktu? Dobrą implementacją kodu hashowego powinno być:

1) Szybko. Bardzo szybko! Pamiętaj, cały sens hash code w pierwszej kolejności jest szybko znaleźć stosunkowo puste miejsce w tabeli hash. Jeśli o(1) obliczenie kodu skrótu jest w praktyce wolniejsze niż O (n) czas potrzebny na naiwne przeszukiwanie, To rozwiązanie kodu skrótu jest stratą netto.

2) dobrze rozłożone w przestrzeni 32-bitowych liczb całkowitych dla danej dystrybucji wejść. Im gorszy rozkład w intach, tym bardziej naiwna będzie liniowa tabela hash.

Więc jak tworzysz algorytm hash dla dowolnych typów wartości biorąc pod uwagę te dwa sprzeczne cele? Każdy czas spędzony na złożonym algorytmie hash, który gwarantuje dobrą dystrybucję, jest czasem słabo spędzonym.

Powszechną sugestią jest "hash wszystkich pól, a następnie XOR razem wynikowe kody hash". Ale to jest pytanie, XORing dwa 32-bitowe wejścia daje dobrą dystrybucję tylko wtedy, gdy same wejścia są bardzo dobrze rozmieszczone i nie są ze sobą powiązane, a to jest mało prawdopodobny scenariusz:

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

Jakie jest prawdopodobieństwo, że x i y są dobrze rozłożone w całym zakresie 32-bitowych liczb całkowitych? Bardzo nisko. Szanse są znacznie lepsze, ponieważ oba są Małe i blisko siebie , w którym to przypadku xoring ich kodów hashowych razem sprawia, że rzeczy gorzej, a nie lepiej. xoring razem liczby całkowite, które są blisko siebie, zeruje większość bitów.

Ponadto, jest to O (n) w liczbie pól! Typ wartości z wiele małych pól wymagałoby stosunkowo dużo czasu, aby obliczyć kod hashowy.

Zasadniczo sytuacja, w jakiej się znajdujemy, polega na tym, że użytkownik sam nie dostarczył implementacji kodu hashowego; albo nie obchodzi go to, albo nie oczekuje, że ten typ zostanie kiedykolwiek użyty jako klucz w tabeli hash. Biorąc pod uwagę, że nie masz żadnych semantycznych Informacji o typie, co najlepiej zrobić? Najlepszą rzeczą do zrobienia jest to, co jest szybkie i daje dobre wyniki przez większość czasu.

Przez większość czasu dwie instancje struktury, które różnią się od siebie, będą się różnić w w większości swoich pól, a nie tylko w jednym z ich pól, więc wybranie jednego z nich i nadzieja, że to ten, który się różni, wydaje się rozsądne.

Przez większość czasu dwie różne instancje struktury będą miały pewną redundancję w swoich polach, więc łączenie wartości skrótu wielu pól razem prawdopodobnie zmniejszy, a nie zwiększy entropię w wartości skrótu, nawet jeśli pochłonie ona czas, który wynosi 20 sekund. algorytm hash jest przeznaczony do zapisywania.

Porównaj to z projektowaniem anonimowych typów w C#. W przypadku typów anonimowych wiemy, że jest bardzo prawdopodobne, że typ jest używany jako klucz do tabeli. Wiemy, że istnieje duże prawdopodobieństwo, że pojawi się redundancja pomiędzy instancjami anonimowych typów (ponieważ są one wynikiem iloczynu kartezjańskiego lub innego połączenia). Dlatego łączymy kody hash wszystkich pól w jeden kod hashowy. Jeśli to daje masz złą wydajność z powodu nadmiernej liczby obliczanych kodów skrótu, możesz używać niestandardowego nominalnego typu, a nie anonimowego.

 31
Author: Eric Lippert,
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
2010-10-01 22:02:06

Rzeczywista implementacja ValueType.GetHashCode() nie do końca pasuje do komentarza. Ma dwie wersje algorytmu, szybką i powolną. Najpierw sprawdza, czy struktura zawiera jakiekolwiek elementy typu reference i czy pomiędzy polami jest jakieś wypełnienie. Padding jest pustą przestrzenią w wartości struktury, utworzoną podczas wyrównywania pól przez kompilator JIT. W strukturze, która zawiera bool i int (3 bajty) jest padding, ale nie ma padding, gdy zawiera int i int, pasują idealnie razem.

Bez referencji i bez wypełnienia, może wykonać szybką wersję, ponieważ każdy bit w wartości struktury jest bitem należącym do wartości pola. Po prostu xors 4 bajty na raz. Otrzymasz "dobry" kod hashowy, który uwzględnia wszystkich członków. Wiele prostych typów struktur w. NET Framework zachowuje się w ten sposób, jak punkt i rozmiar.

Nie zdając tego testu, robi powolną wersję, moralny odpowiednik refleksji. To właśnie dostajesz, Twój KeyValuePair zawiera referencje. A ten sprawdza tylko pole pierwszego kandydata, tak jak w komentarzu. Jest to z pewnością optymalizacja perf, unikając spalania zbyt dużo czasu.

Tak, paskudny szczegół i mało znany. Jest to zwykle odkrywane, gdy ktoś zauważa, że ich kod kolekcji ssie błoto.

Jeszcze jeden potworny szczegół: wersja fast ma błąd, który bajtuje, gdy struktura zawiera pole typu decimal. Wartości 12m i 12,0 m są logicznie równe, ale nie mają ten sam wzór bitowy. GetHashCode() powie, że nie są sobie równe. AUĆ.

 42
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
2014-05-16 16:38:07

Powinien być zgodny z umową GetHashCode, nawet jeśli kolejność pól się zmieni: równe wartości będą miały równe kody hash w ciągu całego życia tego procesu.

W szczególności:

  • nie równe wartości nie muszą mieć nie równe kody hash
  • kody Hash nie muszą być spójne w różnych procesach (możesz zmienić implementację, odbudować i wszystko powinno nadal działać - zasadniczo nie powinieneś utrzymywać kodów hash)

Teraz nie mówię, że Implementacja ValueType jest świetnym pomysłem - spowoduje to spadek wydajności na różne sposoby... ale nie wydaje mi się, żeby to było naprawdę zepsute.

 7
Author: Jon Skeet,
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
2010-10-01 17:32:27

Każda implementacja GetHashCode() ma swoje plusy i minusy. Są to oczywiście rzeczy, które rozważamy podczas wdrażania naszych własnych, ale w przypadku ValueType.GetHashCode() istnieje szczególna trudność w tym, że nie mają zbyt wielu informacji na temat rzeczywistych szczegółów konkretnego typu będą. Oczywiście często zdarza się to nam, gdy tworzymy klasę abstrakcyjną lub taką, która ma być bazą klas, które dodadzą o wiele więcej pod względem stanu, ale w tych przypadkach mamy oczywistą rozwiązanie polegające na użyciu domyślnej implementacji object.GetHashCode(), chyba że klasa pochodna dba o jej nadpisanie.

Z ValueType.GetHashCode() nie mają tego luksusu, ponieważ podstawową różnicą między typem wartości a typem referencyjnym jest, pomimo popularności mówienia o szczegółach implementacji stosu vs. sterty, fakt, że dla równoważności typu wartości odnosi się do wartości, podczas gdy dla równoważności typu obiektu odnosi się do tożsamości (nawet jeśli obiekt definiuje inną formę równoważności przez nadrzędne Equals() i GetHashCode() pojęcie równości odniesienia nadal istnieje i jest nadal użyteczne.

Tak więc, dla metody Equals() implementacja jest oczywista; sprawdź, czy oba obiekty są tego samego typu, a jeśli tak, sprawdź również, czy wszystkie pola są równe (w rzeczywistości istnieje optymalizacja, która w niektórych przypadkach wykonuje bitowe porównanie, ale jest to optymalizacja na tej samej podstawowej idei).

Co zrobić dla GetHashCode()? Po prostu nie ma idealnego rozwiązania. Mogą zrobić tylko kilka rodzaj mult-then-add lub shift-then-xor na każdym polu. Prawdopodobnie dałoby to całkiem dobry hashcode, ale mogłoby być drogie, gdyby było dużo pól (nieważne, że nie zaleca się posiadania typów wartości, które mają wiele pól, wykonawca musi wziąć pod uwagę, że nadal mogą, a nawet mogą być czasy, w których ma to sens, chociaż szczerze nie mogę sobie wyobrazić czasu, w którym zarówno ma to sens, jak i ma również sens, aby go hashować). Gdyby wiedzieli, że niektóre pola rzadko się różnią między instancjami mogą ignorować te pola i nadal mają całkiem dobry hashcode, a jednocześnie są dość szybkie. Na koniec mogą ignorować większość pól i mieć nadzieję, że te, których nie ignorują, przez większość czasu różnią się wartością. Wybrali najbardziej ekstremalną wersję tego ostatniego.

(kwestia tego, co się robi, gdy nie ma pól instancji, to inna sprawa i całkiem dobry wybór, takie typy wartości są równe wszystkim innym instancjom tego samego typu i mają hashcode, który pasuje do tego).

Jest to więc implementacja, która jest do bani, jeśli hashujesz wiele wartości, gdzie pierwsze pole jest takie samo( lub w inny sposób zwraca ten sam hashcode), ale inne implementacje byłyby do bani w innych przypadkach(Mono idzie na xoring wszystkich hashcodów pól razem, lepiej w Twoim przypadku, gorzej w innych).

Kwestia zmiany kolejności pól nie ma znaczenia, ponieważ hashcode jest wyraźnie określony jako ważny tylko przez cały okres istnienia procesu i nie jest nadaje się do większości przypadków, w których mogą być utrzymywane poza tym (może być przydatny w niektórych sytuacjach buforowania, gdzie nie boli, jeśli rzeczy nie zostaną znalezione poprawnie po zmianie kodu).

Więc, nie świetnie, ale nic nie byłoby idealne. Pokazuje to, że zawsze trzeba brać pod uwagę obie strony tego, co oznacza "równość", używając obiektu jako klucza. Można to łatwo naprawić w Twoim przypadku za pomocą:
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);
  }
}

Użyj tego jako komparatora podczas tworzenia słownika, a wszystko powinno być dobrze (ty trzeba tylko generycznych metod porównawczych naprawdę, ale pozostawienie reszty nie szkodzi i może być przydatne czasami).

 3
Author: Jon Hanna,
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
2010-10-01 17:59:05

Dziękuję wszystkim za bardzo, bardzo pouczające odpowiedzi. Wiedziałem, że w tej decyzji musi być jakieś uzasadnienie, ale chciałbym, żeby była lepiej udokumentowana. Nie jestem w stanie użyć V4 frameworka, więc nie ma Tuple<>, i to był główny powód, dla którego zdecydowałem się użyć KeyValuePair struct. Ale myślę, że nie ma żadnych skrótów i będę musiał toczyć własne. Jeszcze raz dziękuję wszystkim.

 0
Author: alh84001,
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
2010-10-02 14:15:56