Bis vor kurzem wäre meine Antwort Jon Skeets hier sehr nahe gekommen. Ich habe jedoch kürzlich ein Projekt gestartet, das Zweierpotenz-Hash-Tabellen verwendet, dh Hash-Tabellen, bei denen die Größe der internen Tabelle 8, 16, 32 usw. beträgt. Es gibt einen guten Grund, Primzahlengrößen zu bevorzugen, aber dort sind auch einige Vorteile für Zweierpotenzgrößen.
Und es saugte ziemlich viel. Nach einigem Experimentieren und Nachforschen begann ich, meine Hashes mit den folgenden Schritten neu zu hashen:
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);
}
}
Und dann saugte meine Zwei-Potenz-Hash-Tabelle nicht mehr.
Das hat mich allerdings gestört, weil das oben genannte nicht funktionieren sollte. Genauer gesagt sollte es nicht funktionieren, es sei denn, das Original GetHashCode()
war auf ganz besondere Weise schlecht.
Das erneute Mischen eines Hashcodes kann einen großartigen Hashcode nicht verbessern, da der einzig mögliche Effekt darin besteht, dass wir einige weitere Kollisionen einführen.
Das erneute Mischen eines Hash-Codes kann einen schrecklichen Hash-Code nicht verbessern, da der einzig mögliche Effekt darin besteht, dass wir beispielsweise eine große Anzahl von Kollisionen auf Wert 53 in eine große Anzahl von Wert 18.3487.291 ändern.
Das erneute Mischen eines Hash-Codes kann nur einen Hash-Code verbessern, der zumindest ziemlich gut absolute Kollisionen in seinem gesamten Bereich (2 32 mögliche Werte) vermeidet, aber Kollisionen schlecht vermeidet, wenn er für die tatsächliche Verwendung in einer Hash-Tabelle heruntergefahren wird. Das einfachere Modulo einer Zweierpotenztabelle machte dies deutlicher, wirkte sich jedoch auch negativ auf die häufigeren Primzahltabellen aus, was jedoch nicht so offensichtlich war (die zusätzliche Arbeit beim Aufwärmen würde den Vorteil überwiegen , aber der Nutzen wäre immer noch da).
Bearbeiten: Ich habe auch Open-Addressing verwendet, was auch die Kollisionsempfindlichkeit erhöht hätte, vielleicht mehr als die Tatsache, dass es sich um eine Zweierpotenz handelt.
Und nun, es war beunruhigend, um wie viel die string.GetHashCode()
Implementierungen in .NET (oder hier zu studieren ) auf diese Weise verbessert werden konnten (in der Größenordnung von Tests, die aufgrund weniger Kollisionen etwa 20 bis 30 Mal schneller ablaufen) und beunruhigender, wie viel meine eigenen Hash-Codes könnte verbessert werden (viel mehr als das).
Alle GetHashCode () -Implementierungen, die ich in der Vergangenheit codiert und tatsächlich als Grundlage für die Antworten auf dieser Site verwendet hatte, waren viel schlechter als ich . Die meiste Zeit war es "gut genug" für viele Zwecke, aber ich wollte etwas Besseres.
Also legte ich dieses Projekt beiseite (es war sowieso ein Lieblingsprojekt) und begann zu überlegen, wie man schnell einen guten, gut verteilten Hash-Code in .NET erstellt.
Am Ende habe ich mich entschlossen , SpookyHash nach .NET zu portieren . In der Tat ist der obige Code eine Fast-Path-Version der Verwendung von SpookyHash, um eine 32-Bit-Ausgabe von einer 32-Bit-Eingabe zu erzeugen.
Jetzt ist SpookyHash kein guter, schnell zu merkender Code. Mein Port davon ist noch weniger, weil ich viel von Hand für eine bessere Geschwindigkeit * eingefügt habe. Aber dafür ist die Wiederverwendung von Code gedacht.
Dann habe ich dieses Projekt beiseite gelegt, weil genau wie das ursprüngliche Projekt die Frage aufgeworfen hatte, wie ein besserer Hash-Code erzeugt werden kann, so hat dieses Projekt die Frage aufgeworfen, wie ein besserer .NET-Speicher erstellt werden kann.
Dann kam ich zurück und erzeugte viele Überladungen, um fast alle nativen Typen (außer decimal
†) einfach in einen Hash-Code einzugeben.
Es ist schnell, wofür Bob Jenkins den größten Teil der Anerkennung verdient, da sein ursprünglicher Code, von dem ich portiert habe, noch schneller ist, insbesondere auf 64-Bit-Computern, für die der Algorithmus optimiert ist.
Der vollständige Code kann unter https://bitbucket.org/JonHanna/spookilysharp/src eingesehen werden. Beachten Sie jedoch, dass der obige Code eine vereinfachte Version davon ist.
Da es jetzt bereits geschrieben ist, kann man es leichter verwenden:
public override int GetHashCode()
{
var hash = new SpookyHash();
hash.Update(field1);
hash.Update(field2);
hash.Update(field3);
return hash.Final().GetHashCode();
}
Es werden auch Startwerte verwendet. Wenn Sie also mit nicht vertrauenswürdigen Eingaben umgehen müssen und sich vor Hash-DoS-Angriffen schützen möchten, können Sie einen Startwert basierend auf der Verfügbarkeit oder ähnlichem festlegen und die Ergebnisse für Angreifer unvorhersehbar machen:
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();
}
* Eine große Überraschung dabei ist das Hand-Inlining einer Rotationsmethode, die (x << n) | (x >> -n)
verbesserte Ergebnisse liefert . Ich wäre mir sicher gewesen, dass der Jitter das für mich eingefügt hätte, aber die Profilerstellung zeigte etwas anderes.
† decimal
ist aus .NET-Sicht nicht nativ, obwohl es aus C # stammt. Das Problem dabei ist, dass seine eigene GetHashCode()
Präzision als signifikant behandelt wird, während seine eigene Equals()
dies nicht tut. Beide sind gültige Entscheidungen, aber nicht so gemischt. Bei der Implementierung Ihrer eigenen Version müssen Sie sich für die eine oder andere Version entscheiden, aber ich kann nicht wissen, welche Sie möchten.
‡ Zum Vergleich. Bei Verwendung für eine Zeichenfolge ist der SpookyHash auf 64 Bit erheblich schneller als string.GetHashCode()
auf 32 Bit, was etwas schneller ist als string.GetHashCode()
auf 64 Bit, was erheblich schneller ist als der SpookyHash auf 32 Bit, obwohl er immer noch schnell genug ist, um eine vernünftige Wahl zu sein.
GetHashCode
. Ich hoffe es wäre hilfreich für andere. Richtlinien und Regeln für GetHashCode geschrieben von Eric Lippert