W .NET GetHashCode
metoda jest używana w wielu miejscach w bibliotekach klas podstawowych .NET. Prawidłowe wdrożenie jest szczególnie ważne, aby szybko znaleźć przedmioty w kolekcji lub określić równość.
Czy istnieje standardowy algorytm lub najlepsza praktyka w zakresie implementacji GetHashCode
dla moich klas niestandardowych, aby nie obniżać wydajności?
.net
algorithm
hashcode
gethashcode
bitbonk
źródło
źródło
GetHashCode
. Mam nadzieję, że będzie to pomocne dla innych. Wytyczne i zasady dotyczące GetHashCode napisane przez Erica LippertaGetHashCode()
jest używany w bardzo wielu implementacjachEquals()
. Właśnie to miałem na myśli z tym stwierdzeniem.GetHashCode()
wewnątrzEquals()
jest często używany jako skrót do określenia nierówności , ponieważ jeśli dwa obiekty mają inny kod skrótu, muszą to być obiekty, które nie są równe, a reszta kontroli równości nie musi zostać wykonana.GetHashCode()
iEquals()
muszą patrzeć na wszystkie pola obu obiektów (Równe musi to zrobić, jeśli kody skrótu są równe lub niezaznaczone). Z tego powodu wezwanie doGetHashCode()
środkaEquals()
jest często zbędne i może obniżyć wydajność.Equals()
może również powodować zwarcie, co znacznie przyspiesza - jednak w niektórych przypadkach kody skrótu mogą być buforowane, co sprawia, żeGetHashCode()
sprawdzenie jest szybsze i bardziej opłacalne. Zobacz to pytanie, aby uzyskać więcej.Odpowiedzi:
Zwykle używam czegoś takiego jak implementacja podana we wspaniałej Effective Java Josh Blocha . Jest szybki i tworzy całkiem niezły skrót, który raczej nie spowoduje kolizji. Wybierz dwie różne liczby pierwsze, np. 17 i 23, i wykonaj:
Jak zauważono w komentarzach, może okazać się, że lepiej jest wybrać dużą liczbę pierwszą do pomnożenia. Najwyraźniej 486187739 jest dobry ... i chociaż większość przykładów, które widziałem z małymi liczbami, zwykle używają liczb pierwszych, istnieją co najmniej podobne algorytmy, w których często używane są liczby inne niż liczby pierwsze. Na przykład w niezupełnie FNV użyłem liczb, które najwyraźniej działają dobrze - ale początkowa wartość nie jest liczbą pierwszą. (Jednak stała mnożenia jest liczbą pierwszą. Nie wiem do końca, jak to jest ważne).
Jest to lepsze niż powszechna praktyka wprowadzania
XOR
kodów mieszających z dwóch głównych powodów. Załóżmy, że mamy typ z dwomaint
polami:Nawiasem mówiąc, wcześniejszy algorytm jest obecnie używany przez kompilator C # dla typów anonimowych.
Ta strona daje całkiem sporo opcji. Myślę, że w większości przypadków powyższe jest „wystarczająco dobre” i jest niezwykle łatwe do zapamiętania i poprawienia. FNV alternatywą jest podobnie prosty, ale stosuje różne stałe i
XOR
zamiastADD
jako łączenie operacji. Wygląda to jak poniższy kod, ale normalny algorytm FNV działa na poszczególnych bajtach, więc wymagałoby to modyfikacji w celu wykonania jednej iteracji na bajt, zamiast na 32-bitową wartość skrótu. FNV jest również zaprojektowany dla zmiennych długości danych, podczas gdy my go tutaj używamy, zawsze dla tej samej liczby wartości pól. Komentarze do tej odpowiedzi sugerują, że kod tutaj nie działa tak dobrze (w testowanym przypadku przykładowym), jak powyższe podejście do dodawania.Należy pamiętać, że jedną rzeczą, o której należy pamiętać, jest to, że najlepiej zapobiegać zmianie stanu wrażliwego na równouprawnienie (a tym samym hashcode) po dodaniu go do kolekcji zależnej od kodu skrótu.
Zgodnie z dokumentacją :
źródło
Dictionary<TKey,TValue>
zakłada dobry rozkład modulo pewnych liczb pierwszych. A 23 jest jednym z nich. Więc jeśli masz słownik z Pojemność 23, tylko ostatni wkład wGetHashCode
wpływanie na złożony hashcode. Wolę więc użyć 29 zamiast 23.null
- co nie jest tym samym, co ignorowanie pola.Typ anonimowy
Microsoft już zapewnia dobry ogólny generator HashCode: Po prostu skopiuj wartości swojej właściwości / pola do anonimowego typu i haszuj go:
Będzie to działać dla dowolnej liczby właściwości. Nie używa boksu. Po prostu używa algorytmu już zaimplementowanego w ramach dla typów anonimowych.
ValueTuple - aktualizacja dla C # 7
Jak wspomniano w komentarzach @cactuaroid, można użyć krotki wartości. Oszczędza to kilka naciśnięć klawiszy i, co ważniejsze, wykonuje się wyłącznie na stosie (bez śmieci):
(Uwaga: Oryginalna technika wykorzystująca anonimowe typy wydaje się tworzyć obiekt na stercie, tj. Śmieci, ponieważ anonimowe typy są implementowane jako klasy, choć kompilator może to zoptymalizować. Ciekawe byłoby przetestowanie tych opcji, ale opcja krotki powinna być lepsza.)
źródło
GetHashCode
implementacja jest bardzo skuteczna (BTW jest taka sama jak ta w odpowiedzi Jona Skeeta), ale jedynym problemem z tym rozwiązaniem jest generowanie nowej instancji przy każdymGetHashCode
wywołaniu. Może to być nieco narzut, szczególnie w przypadku intensywnego dostępu do dużych kolekcji z haszowaniem ...new { PropA, PropB, PropC, PropD }.GetHashCode()
też powiedziećNew With {Key PropA}.GetHashCode()
W przeciwnym razie GetHashCode nie zwróci tego samego kodu skrótu dla różnych obiektów o tych samych właściwościach „identyfikujących”.Oto mój pomocnik hashcode.
Zaletą jest to, że używa argumentów typu ogólnego i dlatego nie powoduje boksowania:
Ma również metodę rozszerzenia, aby zapewnić płynny interfejs, dzięki czemu można go używać w następujący sposób:
lub tak:
źródło
T[]
osobnego, ponieważ jest jużIEnumerable<T>
Mam klasę Hashing w bibliotece Pomocnika, której używam do tego celu.
Następnie możesz po prostu użyć go jako:
Nie oceniłem jego wydajności, więc wszelkie opinie są mile widziane.
źródło
unchecked
wyjątek przepełnienia”. Chodzi o to, aby uniknąć wyjątków dotyczących przepełnienia, które są pożądaneGetHashCode
. Więc nie jest niepoprawne, jeśli wartość się przepełniaint
i wcale nie boli.null
został całkowicie pominięty, może dać nieoczekiwane rezultaty. Zamiast ich pomijać, powinieneś użyć stałej wartości zamiastinput[i].GetHashCode()
kiedyinput[i]
null.Oto moja klasa pomocnicza wykorzystująca implementację Jona Skeeta .
Stosowanie:
Jeśli chcesz uniknąć pisania metody rozszerzenia dla System.Int32:
Nadal unika się alokacji sterty i jest używany dokładnie w ten sam sposób:
Edycja (maj 2018):
EqualityComparer<T>.Default
getter jest teraz nieodłączną częścią JIT - prośba o ściągnięcie jest wspomniana przez Stephena Touba w tym poście na blogu .źródło
var h = Equals(obj, default(T)) ? 0 : obj.GetHashCode();
obj != null
skompiluje się dobox
instrukcji, która przydzieli pamięć, jeśliT
jest typem wartości. Zamiast tego możesz użyć,obj.Equals(null)
który skompiluje się do wirtualnego wywołaniaEquals
metody.this.hashCode != h
. Nie zwróci tej samej wartości..NET Standard 2.1 i wyżej
Jeśli używasz .NET Standard 2.1 lub nowszej wersji, możesz użyć struktury System.HashCode . Istnieją dwie metody korzystania z niego:
HashCode.Combine
Combine
Metoda może być stosowana do tworzenia kod skrótu, podane do ośmiu obiektów.HashCode.Add
Add
Metoda pomaga radzić sobie z kolekcji:Łatwe GetHashCode
Możesz przeczytać pełny wpis na blogu „ GetHashCode Made Easy ”, aby uzyskać więcej informacji i komentarzy.
Przykład użycia
Realizacja
Co sprawia, że dobry algorytm?
Prędkość
Algorytm obliczający kod skrótu musi być szybki. Prosty algorytm zwykle będzie szybszy.
Deterministyczny
Algorytm mieszania musi być deterministyczny, tzn. Przy takim samym wejściu zawsze musi generować ten sam wynik.
Ogranicz kolizje
Algorytm obliczający kod skrótu musi utrzymywać kolizje skrótu na minimalnym poziomie. Kolizja skrótu to sytuacja, w której dwa wywołania
GetHashCode
dwóch różnych obiektów generują identyczne kody skrótu. Należy pamiętać, że kolizje są dozwolone (niektóre mają błędne przekonanie, że nie są), ale należy je ograniczyć do minimum.Dobra funkcja skrótu powinna odwzorowywać oczekiwane dane wejściowe możliwie równomiernie w całym zakresie wyjściowym. Powinien mieć jednolitość.
Prevent's DoS
W .NET Core przy każdym ponownym uruchomieniu aplikacji otrzymasz różne kody skrótu. Jest to funkcja bezpieczeństwa zapobiegająca atakom typu Denial of Service (DoS). W przypadku .NET Framework należy włączyć tę funkcję, dodając następujący plik App.config:
Z powodu tej funkcji kody skrótu nigdy nie powinny być używane poza domeną aplikacji, w której zostały utworzone, nigdy nie powinny być używane jako pola kluczowe w kolekcji i nigdy nie powinny być utrwalane.
Przeczytaj więcej na ten temat tutaj .
Kryptograficznie bezpieczny?
Algorytm nie musi być kryptograficzną funkcją skrótu . Oznacza to, że nie musi spełniać następujących warunków:
źródło
W większości przypadków, gdy Equals () porównuje wiele pól, tak naprawdę nie ma znaczenia, czy twoja funkcja GetHash () ma skrót na jednym polu, czy na wielu. Musisz tylko upewnić się, że obliczanie wartości skrótu jest naprawdę tanie ( bez przydziałów , proszę) i szybkie ( bez ciężkich obliczeń i na pewno żadnych połączeń z bazą danych) i zapewnia dobrą dystrybucję.
Podnoszenie ciężarów powinno być częścią metody Equals (); skrót powinien być bardzo tanią operacją, aby umożliwić wywołanie Equals () na jak najmniejszej liczbie elementów.
I ostatnia wskazówka: nie polegaj na tym, że GetHashCode () jest stabilny w wielu uruchomieniach aplikacji . Wiele typów .Net nie gwarantuje, że ich kody skrótu pozostaną takie same po ponownym uruchomieniu, więc powinieneś używać wartości GetHashCode () tylko w strukturach pamięci.
źródło
GetHashCode
przydziałem pamięci, pod warunkiem, że robi to tylko przy pierwszym użyciu (przy kolejnych wywołaniach po prostu zwraca wynik z pamięci podręcznej). Ważną rzeczą nie jest to, że należy starać się unikać kolizji, ale raczej unikać kolizji „systemowych”. Jeśli typ ma dwaint
polaoldX
inewX
które często różnią się o jedną wartość hasholdX^newX
byłoby przypisanie 90% takich zapisów wartości hash 1, 2, 4 lub 8. KorzystanieoldX+newX
[niezaznaczone arytmetyka] może generować więcej kolizji ...Do niedawna moja odpowiedź była bardzo bliska Jona Skeeta tutaj. Jednak niedawno rozpocząłem projekt wykorzystujący potęgę dwóch tablic mieszających, czyli tabel mieszających, w których wielkość wewnętrznego stołu wynosi 8, 16, 32 itd. Jest dobry powód, aby faworyzować rozmiary liczb pierwszych, ale jest mają również zalety w stosunku do mocy dwóch rozmiarów.
I to prawie do dupy. Więc po odrobinie eksperymentów i badań zacząłem ponownie mieszać moje skróty z następującymi:
A potem mój stół z potęgą dwóch mocy już nie ssał.
Niepokoiło mnie to, ponieważ powyższe nie powinno działać. A dokładniej, nie powinno działać, chyba że oryginał
GetHashCode()
był ubogi w bardzo szczególny sposób.Ponowne mieszanie kodu skrótu nie może poprawić świetnego kodu skrótu, ponieważ jedynym możliwym efektem jest wprowadzenie kilku dodatkowych kolizji.
Ponowne mieszanie kodu skrótu nie może poprawić okropnego kodu skrótu, ponieważ jedynym możliwym efektem jest zmiana np. Dużej liczby kolizji o wartości 53 na dużą liczbę o wartości 18.3487,291.
Ponowne mieszanie kodu skrótu może tylko poprawić kod skrótu, który co najmniej całkiem dobrze radził sobie w unikaniu bezwzględnych kolizji w całym zakresie (2 32 możliwe wartości), ale źle w unikaniu kolizji, gdy został wyłączony do faktycznego użycia w tabeli skrótów. Chociaż prostsze modulo tabeli potęgi dwóch sprawiło, że stało się to bardziej widoczne, miało to również negatywny wpływ na bardziej powszechne tabele liczb pierwszych, ale to po prostu nie było tak oczywiste (dodatkowa praca przy przerobieniu przeważałaby nad korzyścią , ale korzyść nadal byłaby dostępna).
Edycja: Używałem również otwartego adresowania, co również zwiększyłoby wrażliwość na kolizję, być może bardziej niż fakt, że była to potęga dwóch.
Cóż, niepokojące było to, w jakim stopniu
string.GetHashCode()
implementacje w .NET (lub studium tutaj ) mogą zostać ulepszone w ten sposób (w kolejności testów uruchamianych około 20-30 razy szybciej z powodu mniejszej liczby kolizji) i bardziej niepokojące, jak bardzo moje własne kody skrótu można poprawić (znacznie więcej).Wszystkie implementacje GetHashCode (), które zakodowałem w przeszłości i których rzeczywiście użyłem jako podstawy odpowiedzi na tej stronie, były znacznie gorsze niż się spodziewałem . Przez większość czasu było to „wystarczająco dobre” do większości zastosowań, ale chciałem czegoś lepszego.
Dlatego odłożyłem ten projekt na bok (zresztą i tak był to projekt dla zwierząt domowych) i zacząłem szukać sposobu szybkiego stworzenia dobrego, dobrze rozproszonego kodu skrótu w .NET.
W końcu zdecydowałem się na przeniesienie SpookyHash do .NET. Rzeczywiście powyższy kod jest szybką wersją używania SpookyHash do tworzenia 32-bitowego wyjścia z 32-bitowego wejścia.
Teraz SpookyHash nie jest łatwym do zapamiętania fragmentem kodu. Mój port jest jeszcze mniejszy, ponieważ ręcznie podłożyłem dużo, aby uzyskać lepszą prędkość *. Ale po to jest ponowne użycie kodu.
Następnie odłożyłem ten projekt na bok, ponieważ tak jak w pierwotnym projekcie pojawiło się pytanie, w jaki sposób stworzyć lepszy kod skrótu, tak że w projekcie pojawiło się pytanie, w jaki sposób stworzyć lepszy memcpy .NET.
Potem wróciłem i spowodowałem wiele przeciążeń, aby łatwo wprowadzić prawie wszystkie rodzime typy (z wyjątkiem
decimal
†) do kodu skrótu.Jest szybki, na co Bob Jenkins zasługuje na największe uznanie, ponieważ jego oryginalny kod, z którego się przeniosłem, jest jeszcze szybszy, szczególnie na komputerach 64-bitowych, dla których algorytm jest zoptymalizowany ‡.
Pełny kod można zobaczyć na https://bitbucket.org/JonHanna/spookilysharp/src, ale należy pamiętać, że powyższy kod jest jego uproszczoną wersją.
Ponieważ jednak jest już napisane, można z niego łatwiej korzystać:
Przyjmuje także wartości początkowe, więc jeśli musisz poradzić sobie z niezaufanym wejściem i chcesz chronić się przed atakami Hash DoS, możesz ustawić ziarno na podstawie czasu działania lub podobnego, a wyniki mogą być nieprzewidywalne dla atakujących:
* Wielką niespodzianką jest to, że ręczne wprowadzanie metody rotacji, która zwróciła
(x << n) | (x >> -n)
ulepszone rzeczy. Byłbym pewien, że jitter podkreśliłby to dla mnie, ale profilowanie pokazało inaczej.†
decimal
nie jest natywny z perspektywy .NET, choć pochodzi z C #. Problem polega na tym, że jego własnaGetHashCode()
traktuje precyzję jako znaczącą, podczas gdy jej własnaEquals()
nie. Oba są prawidłowymi wyborami, ale nie są tak mieszane. Wdrażając własną wersję, musisz wybrać jedną lub drugą, ale nie wiem, czego chcesz.‡ Dla porównania. W przypadku użycia ciągu znaków SpookyHash na 64 bitach jest znacznie szybszy niż
string.GetHashCode()
na 32 bitach, co jest nieco szybszy niżstring.GetHashCode()
na 64 bitach, co jest znacznie szybszy niż SpookyHash na 32 bitach, choć wciąż wystarczająco szybki, aby być rozsądnym wyborem.źródło
long
wartości dla wyników pośrednich, a następnie munge końcowy wynik w dół doint
. Czy to wydaje się dobrym pomysłem? Obawiam się, że używa się np. Hash = (hash * 31) + nextField, wtedy pary pasujących wartości wpłyną tylko na górne 27 bitów skrótu. Zezwolenie na obliczenialong
i zawinięcie rzeczy zminimalizuje to niebezpieczeństwo..Update()
z wieloma wartościami zgodnie z powyższą odpowiedzią załatwi sprawę.Ten jest dobry:
A oto jak go użyć:
źródło
GetHashCode()
metodę, więc zawsze możesz użyć tej metody zparams
parametrem tablica. A może coś tu brakuje?h += (h << 10); h ^= (h >> 6); h += (h << 3); h ^= (h >> 11); h += (h << 15);
mają zapachy kodu: nie zależą od dowolnego wejścia i wyglądają strasznie redundantny do mnie.Począwszy od https://github.com/dotnet/coreclr/pull/14863 , istnieje nowy sposób generowania kodów skrótu, który jest bardzo prosty! Tylko napisz
Spowoduje to wygenerowanie wysokiej jakości kodu skrótu bez konieczności martwienia się o szczegóły implementacji.
źródło
HashCode
zmiany w corefx zostały scalone na kilka godzin przed twoim komentarzem :) Ten typ ma się pojawić w .NET Core 2.1.Oto kolejna płynna implementacja algorytmu opublikowanego powyżej przez Jona Skeeta , ale która nie obejmuje alokacji ani operacji bokserskich:
Stosowanie:
Kompilator zapewni, że
HashValue
nie zostanie wywołany z klasą ze względu na ogólne ograniczenie typu. Ale nie ma wsparcia dla kompilatora,HashObject
ponieważ dodanie ogólnego argumentu dodaje również operację boksu.źródło
Oto moje uproszczone podejście. Używam do tego klasycznego wzorca konstruktora. Jest bezpieczny dla typów (bez boxowania / rozpakowywania), a także kompatybilny z .NET 2.0 (bez metod rozszerzenia itp.).
Używa się go w następujący sposób:
A oto klasa klasycznego budowniczego:
źródło
AddItems<T>(params T[] items)
częściej używać metody w klasie pomocniczej (niż wywoływać zaAddItem(T)
każdym razem).this.result * Prime2 * item.GetHashCode()
często używanethis.result * Prime2 + item.GetHashCode()
?AddItems<T>(params T[] items)
częściej, ponieważtypeof(T1) != typeof(T2)
itp.Użytkownicy ReSharper mogą generować GetHashCode, Equals i inne za pomocą
ReSharper -> Edit -> Generate Code -> Equality Members
.źródło
Jeśli mamy nie więcej niż 8 właściwości (mam nadzieję), oto kolejna alternatywa.
ValueTuple
jest strukturą i wydaje się mieć solidnąGetHashCode
implementację.Oznacza to, że możemy po prostu to zrobić:
Rzućmy okiem na obecnej implementacji NET rdzenia za
ValueTuple
„sGetHashCode
.To jest z
ValueTuple
:A to z
HashHelper
:Po angielsku:
Byłoby miło wiedzieć więcej o właściwościach tego algorytmu kodu skrótu ROL-5.
Niestety odroczenie się
ValueTuple
w naszym przypadkuGetHashCode
może nie być tak szybkie, jak byśmy tego oczekiwali. Ten komentarz w powiązanej dyskusji pokazuje, że bezpośrednie wywoływanieHashHelpers.Combine
jest bardziej wydajne. Z drugiej strony, ten jest wewnętrzny, więc musielibyśmy skopiować kod, poświęcając wiele z tego, co tutaj zyskaliśmy. Bylibyśmy także odpowiedzialni za pamiętanieCombine
o losowym nasieniu. Nie wiem, jakie będą konsekwencje pominięcia tego kroku.źródło
h1 >> 27
zignorujesz to 0,h1 << 5
jesth1 * 32
więc równeh1 * 33 ^ h2
. Według tej strony nazywa się to „Zmodyfikowany Bernstein”.Większość mojej pracy polega na łączności z bazą danych, co oznacza, że wszystkie moje klasy mają unikalny identyfikator z bazy danych. Zawsze używam identyfikatora z bazy danych, aby wygenerować kod skrótu.
źródło
_id.GetHashCode
co jest jasne.Prawie podobne do rozwiązania nightcodera, tyle że łatwiej jest podnieść liczby pierwsze, jeśli chcesz.
PS: To jeden z tych momentów, w których rzygasz trochę w usta, wiedząc, że można to zmienić na jedną z 9 domyślnych metod, ale byłoby wolniejsze, więc po prostu zamknij oczy i spróbuj o tym zapomnieć.
źródło
Wystąpił problem z liczbami zmiennoprzecinkowymi i dziesiętnymi przy użyciu implementacji wybranej jako odpowiedź powyżej.
Ten test kończy się niepowodzeniem (liczba zmiennoprzecinkowa; skrót jest taki sam, mimo że zmieniłem 2 wartości na ujemne):
Ale ten test przechodzi (z ints):
Zmieniłem implementację, aby nie używać GetHashCode dla typów pierwotnych i wydaje się, że działa lepiej
źródło
unchecked
nie wpływa naConvert.ToInt32
:uint
,long
,float
,double
idecimal
wszystko może przepełnienie tutaj.Microsoft prowadzi na kilka sposobów mieszania ...
Domyślam się, że dla wielu dużych int możesz użyć tego:
To samo dotyczy wielu typów: wszystkie przekonwertowane najpierw na
int
użycie,GetHashCode()
a następnie wartości int zostaną xor'owane, a wynikiem będzie twój skrót.Dla tych, którzy używają skrótu jako ID (mam na myśli unikalną wartość), skrót jest naturalnie ograniczony do kilku cyfr, myślę, że było to 5 bajtów dla algorytmu skrótu, przynajmniej MD5.
Możesz zamienić wiele wartości na wartość mieszaną, a niektóre z nich są takie same, więc nie używaj jej jako identyfikatora. (może kiedyś użyję twojego komponentu)
źródło
Jest to statyczna klasa pomocnicza, która implementuje implementację Josha Blocha; i zapewnia wyraźne przeciążenia, aby „zapobiec” boksowaniu, a także implementować skrót specjalnie dla długich operacji podstawowych.
Możesz przekazać ciąg znaków, który pasuje do twojej równej implementacji.
Ponieważ wyjście Hash jest zawsze int, możesz po prostu łączyć wywołania Hash.
źródło
HashKeysAndValues
Metoda została ustalona: to wywołujeHashKeyAndValue
.W przypadku, gdy chcesz PolyFill
HashCode
odnetstandard2.1
Uwaga: Jeśli zostanie użyty z
struct
, przydzieli pamięć ze względu na boksźródło