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:
public static int ReHash(int source)
{
unchecked
{
ulong c = 0xDEADBEEFDEADBEEF + (ulong)source;
ulong d = 0xE2ADBEEFDEADBEEF ^ c;
ulong a = d += c = c << 15 | c >> -15;
ulong b = a += d = d << 52 | d >> -52;
c ^= b += a = a << 26 | a >> -26;
d ^= c += b = b << 51 | b >> -51;
a ^= d += c = c << 28 | c >> -28;
b ^= a += d = d << 9 | d >> -9;
c ^= b += a = a << 47 | a >> -47;
d ^= c += b << 54 | b >> -54;
a ^= d += c << 32 | c >> 32;
a += d << 25 | d >> -25;
return (int)(a >> 1);
}
}
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ć:
public override int GetHashCode()
{
var hash = new SpookyHash();
hash.Update(field1);
hash.Update(field2);
hash.Update(field3);
return hash.Final().GetHashCode();
}
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:
private static long hashSeed0 = Environment.TickCount;
private static long hashSeed1 = DateTime.Now.Ticks;
public override int GetHashCode()
{
//produce different hashes ever time this application is restarted
//but remain consistent in each run, so attackers have a harder time
//DoSing the hash tables.
var hash = new SpookyHash(hashSeed0, hashSeed1);
hash.Update(field1);
hash.Update(field2);
hash.Update(field3);
return hash.Final().GetHashCode();
}
* 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łasna GetHashCode()
traktuje precyzję jako znaczącą, podczas gdy jej własna Equals()
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.
GetHashCode
. Mam nadzieję, że będzie to pomocne dla innych. Wytyczne i zasady dotyczące GetHashCode napisane przez Erica Lipperta