Schnelle und einfache Hash-Code-Kombinationen

70

Können Leute schnelle und einfache Möglichkeiten empfehlen, um die Hash-Codes zweier Objekte zu kombinieren? Ich mache mir keine allzu großen Sorgen um Kollisionen, da ich eine Hash-Tabelle habe, die so effizient funktioniert. Ich möchte nur etwas, das so schnell wie möglich einen Code generiert.

Beim Lesen in SO und im Internet scheint es einige Hauptkandidaten zu geben:

  1. XORing
  2. XORing mit Prime Multiplication
  3. Einfache numerische Operationen wie Multiplikation / Division (mit Überlaufprüfung oder Umwickeln)
  4. Erstellen eines Strings und anschließende Verwendung der Hash-Code-Methode der String-Klassen

Was würden die Leute empfehlen und warum?

RobV
quelle

Antworten:

120

Ich persönlich würde XOR vermeiden - es bedeutet, dass zwei gleiche Werte zu 0 führen - also Hash (1, 1) == Hash (2, 2) == Hash (3, 3) usw. Auch Hash (5, 0) == Hash (0, 5) usw., der gelegentlich auftreten kann. Ich habe es absichtlich für das Set-Hashing verwendet. Wenn Sie eine Folge von Elementen hashen möchten und sich nicht um die Bestellung kümmern, ist es schön.

Ich benutze normalerweise:

unchecked
{
    int hash = 17;
    hash = hash * 31 + firstField.GetHashCode();
    hash = hash * 31 + secondField.GetHashCode();
    return hash;
}

Das ist die Form, die Josh Bloch in Effective Java vorschlägt. Als ich das letzte Mal eine ähnliche Frage beantwortete, fand ich einen Artikel, in dem dies ausführlich besprochen wurde - IIRC, niemand weiß wirklich, warum es gut funktioniert, aber es funktioniert. Es ist auch leicht zu merken, leicht zu implementieren und auf eine beliebige Anzahl von Feldern zu erweitern.

Jon Skeet
quelle
4
Sieht aus wie Dan Bernsteins (oder Chris Toreks) Hash, nur mit verschiedenen Konstanten. Niemand weiß, warum das auch gut funktioniert.
Ephemient
11
Ein Wort der Warnung, dies ist (eine Variation von) dem Berstein-Hash, und da niemand weiß, warum er in Tests gut abschneidet, ist es nicht ratsam, wenn das Hashing kritisch ist. Siehe eternallyconfuzzled.com/tuts/algorithms/jsw_tut_hashing.aspx . Außerdem sollten Sie diesen Code in einen unchecked { }Block einschließen. GetHashCode () sollte keine Ausnahmen auslösen.
Henk Holterman
5
@tofutim: Die 31 ist eine gute Wahl, da die Multiplikation mit 31 auf eine Verschiebung und Subtraktion optimiert werden kann. Ob es ist optimiert auf diese Weise auf der Plattform abhängt. Warum diese Zahlen gut zum Hashing geeignet sind - wie Henk sagt, ist ein bisschen rätselhaft.
Jon Skeet
3
@ rory.ap: Ich denke, es ist eine hervorragende Arbeit, und ich würde diese Zahlen gerne verwenden. Obwohl ich es hasse zuzugeben, Konstanten zu verwenden ", weil jemand anderes zu" sagte ", geht es im Grunde genommen um das 17/31-Paar.
Jon Skeet
12
Ab .NET Core 2.1 können Sie die Combine-Methode des System.HashCode-Typs verwenden, um dies zu tun. Docs.microsoft.com/en-us/dotnet/api/system.hashcode.combine
Cosmin Sontu
52

Während die in Jon Skeets Antwort beschriebene Vorlage im Allgemeinen als Hash-Funktionsfamilie gut funktioniert, ist die Auswahl der Konstanten wichtig, und der Keim 17und der Faktor von, 31wie in der Antwort angegeben, funktionieren für allgemeine Anwendungsfälle überhaupt nicht gut. In den meisten Anwendungsfällen liegen die Hash-Werte viel näher bei Null als int.MaxValueund die Anzahl der gemeinsam gehashten Elemente beträgt einige Dutzend oder weniger.

Für das Hashing eines ganzzahligen Tupels {x, y}mit -1000 <= x <= 1000und -1000 <= y <= 1000hat es eine abgrundtiefe Kollisionsrate von fast 98,5%. Zum Beispiel {1, 0} -> {0, 31}, {1, 1} -> {0, 32}usw. Wenn wir die Abdeckung erweitern , um auch n-Tupel umfasst , wo 3 <= n <= 25, tut es weniger schrecklich mit einer Kollisionsgeschwindigkeit von etwa 38%. Aber wir können es viel besser machen.

public static int CustomHash(int seed, int factor, params int[] vals)
{
    int hash = seed;
    foreach (int i in vals)
    {
        hash = (hash * factor) + i;
    }
    return hash;
}

Ich habe eine Monte-Carlo-Stichproben-Suchschleife geschrieben, die die obige Methode mit verschiedenen Werten für Startwert und Faktor über verschiedene zufällige n-Tupel zufälliger Ganzzahlen getestet hat i. Zulässige Bereiche waren 2 <= n <= 25(wo nzufällig, aber zum unteren Ende des Bereichs hin voreingenommen) und -1000 <= i <= 1000. Für jedes Samen- und Faktorpaar wurden mindestens 12 Millionen einzigartige Kollisionstests durchgeführt.

Nach ca. 7 Stunden Laufzeit , das beste Paar gefunden (wo der Samen und Faktor sowohl auf 4 Stellen begrenzt waren oder weniger) war: seed = 1009, factor = 9176, mit einer Kollisionsgeschwindigkeit von 0,1131%. In den 5- und 6-stelligen Bereichen gibt es noch bessere Optionen. Aber ich habe der Kürze halber den besten 4-stelligen Darsteller ausgewählt, und er zeigt sich in allen gängigen intund charHashing-Szenarien recht gut . Es scheint auch gut mit ganzen Zahlen von viel größeren Größen zu funktionieren.

Es ist erwähnenswert, dass "Prime sein" keine allgemeine Voraussetzung für eine gute Leistung als Keim und / oder Faktor zu sein schien, obwohl dies wahrscheinlich hilfreich ist. 1009oben erwähnt ist in der Tat Prime, ist es aber 9176nicht. Ich habe explizit Variationen davon getestet, bei denen ich factorzu verschiedenen Primzahlen in der Nähe 9176(während des Verlassens seed = 1009) gewechselt habe und alle schlechter abschnitten als die obige Lösung.

Zuletzt habe ich auch mit der generischen ReSharper-Empfehlungsfunktionsfamilie von verglichen hash = (hash * factor) ^ i;und das Original, CustomHash()wie oben erwähnt, übertrifft es ernsthaft. Der ReSharper XOR-Stil scheint Kollisionsraten im Bereich von 20 bis 30% für allgemeine Anwendungsfallannahmen zu haben und sollte meiner Meinung nach nicht verwendet werden.

Spezielle Sauce
quelle
8
Beeindruckend. Ich liebe die Arbeit, die in diese Antwort geflossen ist. Beeindruckend, gut gemacht!
Tom Leys
Scheint das Beste zu sein, aber es gibt zwei Bemerkungen: Erstens und einfach, warum nicht "Samen" und "Faktor" an das Ende verschieben und ihnen einen Standardwert (1009 und 9176) geben, wo sie die Arbeit für die meisten Menschen erledigen sollen. Zweiter Punkt: Wie bei Jon Skeet algo ist dies auftragsabhängig und Sie können einen anderen Hash erhalten, wenn Sie in einer anderen Reihenfolge eingeben. Ich frage mich, ob es nicht sicherer wäre, dieses Array zuerst zu sortieren, um sicherzustellen, dass am Ende derselbe endgültige Hash angezeigt wird, auch wenn Sie den Algo auf unterschiedliche Weise füttern. Das würde sicherer werden.
Eric Ouellet
1
@EricOuellet Da das params int[] valsam Ende aller Funktionsargumente stehen muss, konnte ich die seedund die Standardparameter nicht festlegen factor. Wenn Sie sich nicht für die paramsSyntaxfreundlichkeit interessieren , können Sie sie entfernen und dann die Parameter neu anordnen, um die von Ihnen vorgeschlagenen Standardeinstellungen zuzulassen.
Spezielle Sauce
@EricOuellet Das Standard-Hashing für ein Array sollte darin bestehen, Permutationen zu berücksichtigen (dies ist der allgemeinere Fall). Daher wäre der Hash für verschiedene Ordnungen unterschiedlich (genauso wie der Hash für die Zeichenfolge "abc" anders ist als der Hash für "acb"). ). Wenn Sie speziell eine Hash-Funktion nur für Kombinationen wünschen, sollten Sie wahrscheinlich ein HashSet<int>Argument akzeptieren (unter der Annahme, dass keine Duplikate vorhanden sind). Andernfalls können Sie die Funktion umbenennen CustomHashCombination(), um Verwirrung zu vermeiden, und die interne Vorsortierung wie vorgeschlagen durchführen.
Spezielle Soße
1
Ich mag diese Antwort, würde sie aber nicht verwenden, paramsda sie bei jedem Aufruf ein Array zuweisen muss. In Bezug auf Berechnungen kann es also schneller sein, aber es erzeugt GC-Druck für später.
Gru
50

Wenn Sie .NET Core 2.1 oder höher oder .NET Framework 4.6.1 oder höher verwenden, sollten Sie die System.HashCode- Struktur verwenden, um zusammengesetzte Hash-Codes zu erstellen . Es gibt zwei Betriebsarten: Hinzufügen und Kombinieren.

Ein Beispiel mit Combine, das normalerweise einfacher ist und für bis zu acht Elemente funktioniert:

public override int GetHashCode()
{
    return HashCode.Combine(object1, object2);
}

Ein Beispiel für die Verwendung von Add:

public override int GetHashCode()
{
    var hash = new HashCode();
    hash.Add(this.object1);
    hash.Add(this.object2);
    return hash.ToHashCode();
}

Vorteile:

Nachteile:

Chwarr
quelle
Sie können auf nuget.org/packages/Microsoft.Bcl.HashCode verweisen , damit es offiziell unter .NET Framework 4.6.1 oder .NET Standard 2.0 funktioniert.
ChrisTorng
19

Verwenden Sie die Kombinationslogik in Tupel. Das Beispiel verwendet c # 7-Tupel.

(field1, field2).GetHashCode();
Yepeekai
quelle
Gute Idee, obwohl ich vermute, dass dies Probleme mit der GC-Abwanderung haben könnte, da Sie implizit ein kurzlebiges Objekt
erstellen
9
@RobV Tupel sind Werttypen, daher werden sie stapelweise zugewiesen und üben keinen GC-Druck aus.
Mike Pedersen
1
Ein Problem ... (0,1,2) .GetHashCode () und (0,0,1,2) .GetHashCode () ergeben beide den gleichen Wert: 35. Während die Methode in der am besten bewerteten Antwort eindeutige Werte 0 ergibt , 1, 2: 506480 und 0, 0, 1, 2: 15699890
Dynamichael
2
Es ist nicht garantiert, dass Hashcodes eindeutig sind. Sie haben einen Fall gefunden, in dem dies nicht der Fall ist ... Es ist keine schlechte Wahl, es sei denn, es gibt viele Kollisionen (in diesem Fall wäre es eine gute Idee, einen Fehler einzureichen). Ich persönlich bevorzuge es, etwas aus dem Framework zu verwenden, anstatt etwas anderes zu implementieren.
Yepeekai
1
Es ist eigentlich eine ValueTupleArt Struktur ( MSDN ). Achten Sie darauf, dass dieser TupleTyp eine Klasse ist und GC-Druck hat. Ich mag diesen Weg. Intern ähnelt es dem Beitrag von @Stipo, ist aber sehr einfach zu verstehen und zu überprüfen. In den meisten Fällen wäre es eine gute Wahl.
Cactuaroid
17

Ich gehe davon aus, dass das .NET Framework-Team beim Testen der Implementierung von System.String.GetHashCode () gute Arbeit geleistet hat, also würde ich es verwenden:

// System.String.GetHashCode(): http://referencesource.microsoft.com/#mscorlib/system/string.cs,0a17bbac4851d0d4
// System.Web.Util.StringUtil.GetStringHashCode(System.String): http://referencesource.microsoft.com/#System.Web/Util/StringUtil.cs,c97063570b4e791a
public static int CombineHashCodes(IEnumerable<int> hashCodes)
{
    int hash1 = (5381 << 16) + 5381;
    int hash2 = hash1;

    int i = 0;
    foreach (var hashCode in hashCodes)
    {
        if (i % 2 == 0)
            hash1 = ((hash1 << 5) + hash1 + (hash1 >> 27)) ^ hashCode;
        else
            hash2 = ((hash2 << 5) + hash2 + (hash2 >> 27)) ^ hashCode;

        ++i;
    }

    return hash1 + (hash2 * 1566083941);
}

Eine weitere Implementierung stammt aus den Methoden System.Web.Util.HashCodeCombiner.CombineHashCodes (System.Int32, System.Int32) und System.Array.CombineHashCodes (System.Int32, System.Int32) . Dieser ist einfacher, hat aber wahrscheinlich keine so gute Verteilung wie die obige Methode:

// System.Web.Util.HashCodeCombiner.CombineHashCodes(System.Int32, System.Int32): http://referencesource.microsoft.com/#System.Web/Util/HashCodeCombiner.cs,21fb74ad8bb43f6b
// System.Array.CombineHashCodes(System.Int32, System.Int32): http://referencesource.microsoft.com/#mscorlib/system/array.cs,87d117c8cc772cca
public static int CombineHashCodes(IEnumerable<int> hashCodes)
{
    int hash = 5381;

    foreach (var hashCode in hashCodes)
        hash = ((hash << 5) + hash) ^ hashCode;

    return hash;
}
Stipo
quelle
2

Dies ist ein Umpacken der brillant recherchierten Lösung von Special Sauce .
Es verwendet Value Tuples ( ITuple).
Dies ermöglicht Standardeinstellungen für die Parameter seedund factor.

public static int CombineHashes(this ITuple tupled, int seed=1009, int factor=9176)
{
    var hash = seed;

    for (var i = 0; i < tupled.Length; i++)
    {
        unchecked
        {
            hash = hash * factor + tupled[i].GetHashCode();
        }
    }

    return hash;
}

Verwendung:

var hash1 = ("Foo", "Bar", 42).CombineHashes();    
var hash2 = ("Jon", "Skeet", "Constants").CombineHashes(seed=17, factor=31);
3dGrabber
quelle
0

Wenn Ihre Eingabe-Hashes dieselbe Größe haben, gleichmäßig verteilt sind und nicht miteinander in Beziehung stehen, sollte ein XOR in Ordnung sein. Außerdem ist es schnell.

Die Situation, für die ich dies vorschlage, ist die, in der Sie dies tun möchten

H = hash(A) ^ hash(B); // A and B are different types, so there's no way A == B.

Wenn erwartet werden kann, dass A und B mit einer angemessenen (nicht zu vernachlässigenden) Wahrscheinlichkeit auf denselben Wert hashen, sollten Sie XOR natürlich nicht auf diese Weise verwenden.

geofftnz
quelle
Wie würde ich feststellen, ob meine Hash-Codes gleichmäßig verteilt sind? Gibt es dafür einen einfachen Benchmark? Ich weiß, dass die Kollisionsrate ziemlich niedrig ist, aber entspricht das notwendigerweise einer gleichmäßigen Verteilung?
RobV
0

Wenn Sie Geschwindigkeit suchen und nicht zu viele Kollisionen haben, ist XOR am schnellsten. Um ein Clustering um Null zu verhindern, können Sie Folgendes tun:

finalHash = hash1 ^ hash2;
return finalHash != 0 ? finalHash : hash1;

Natürlich sollten einige Prototypen Ihnen eine Vorstellung von Leistung und Clustering geben.

Ed Power
quelle
-1

Angenommen, Sie haben eine relevante toString () -Funktion (in der Ihre verschiedenen Felder angezeigt werden sollen), würde ich nur den Hashcode zurückgeben:

this.toString().hashCode();

Dies ist nicht sehr schnell, sollte aber Kollisionen recht gut vermeiden.

Thomas Hugel
quelle
-11

Ich würde empfehlen, die in System.Security.Cryptography integrierten Hash-Funktionen zu verwenden, anstatt Ihre eigenen zu rollen.

richardtallent
quelle
11
Nein, sie haben einen ganz anderen Zweck und brechen die Regel, dass GetHashCode schnell sein sollte.
Henk Holterman