it-swarm.com.de

Schnelle und einfache Hash-Code-Kombinationen

Können Leute schnelle und einfache Methoden empfehlen, um die Hash-Codes zweier Objekte zu kombinieren. Ich mache mir keine Sorgen um Kollisionen, da ich eine Hash-Tabelle habe, die so effizient gehandhabt wird. Ich möchte nur etwas, das einen Code so schnell wie möglich generiert.

Beim Lesen von SO und im Web scheinen einige Hauptkandidaten zu sein:

  1. XORing
  2. XORing mit Prime Multiplication
  3. Einfache numerische Operationen wie Multiplikation/Division (mit Überlaufprüfung oder Umbruch)
  4. Erstellen Sie einen String und verwenden Sie dann die Hash-Code-Methode der String-Klassen

Was würden die Leute empfehlen und warum?

50
RobV

Ich persönlich würde XOR vermeiden - das 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) etc was gelegentlich auftaucht. Ich habe habe es absichtlich für Set-Hashing verwendet - wenn Sie eine Sequenz von Elementen hashen möchten und sich nicht um die Bestellung kümmern, ist es nett.

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, einfach zu implementieren und auf eine beliebige Anzahl von Feldern zu erweitern.

103
Jon Skeet

Während die in der Antwort von Jon Skeet umrissene Vorlage im Allgemeinen als Hash-Funktionsfamilie gut funktioniert, ist die Wahl der Konstanten wichtig, und der in der Antwort erwähnte Keim von 17 und Faktor von 31 funktioniert für allgemeine Anwendungsfälle überhaupt nicht. In den meisten Anwendungsfällen liegen die Hashwerte viel näher an Null als int.MaxValue, und die Anzahl der gemeinsam gehashten Elemente beträgt einige Dutzend oder weniger.

Für das Hashing eines ganzzahligen Tupels {x, y}, wobei -1000 <= x <= 1000 und -1000 <= y <= 1000 eine Kollisionsrate von nahezu 98,5% aufweisen. Zum Beispiel {1, 0} -> {0, 31}, {1, 1} -> {0, 32} usw. Wenn wir die Abdeckung um n-Tupel erweitern, bei denen 3 <= n <= 25 verwendet wird, ist dies mit einer Kollisionsrate von etwa 38% weniger schlimm. Aber wir können 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 schrieb eine Monte-Carlo-Sampling-Suchschleife, bei der die obige Methode mit verschiedenen Werten für Startwert und Faktor über verschiedene zufällige n-Tupel von Zufallszahlen i getestet wurde. Zulässige Bereiche waren 2 <= n <= 25 (wobei n zufällig, aber zum unteren Ende des Bereichs geneigt war) und -1000 <= i <= 1000. Mindestens 12 Millionen eindeutige Kollisionstests wurden für jedes Saat- und Faktorpaar durchgeführt.

Nach ungefähr 7 Stunden war das beste gefundene Paar (wobei der Samen und der Faktor beide auf 4 Stellen oder weniger begrenzt waren): seed = 1009, factor = 9176 mit einer Kollisionsrate von 0,1131%. Im 5- und 6-stelligen Bereich gibt es noch bessere Möglichkeiten. Ich habe den besten 4-stelligen Darsteller aus Gründen der Kürze ausgewählt, und er ist in allen gängigen int- und char-Hash-Szenarien ziemlich gut. Es scheint auch gut mit ganzzahligen Zahlen zu funktionieren.

Es ist erwähnenswert, dass "Prime sein" keine allgemeine Voraussetzung für eine gute Leistung als Saatgut und/oder Faktor zu sein schien, obwohl es wahrscheinlich hilft. Der oben angegebene 1009 ist in der Tat eine Primzahl, aber 9176 nicht. Ich habe explizit Variationen getestet, bei denen ich factor in verschiedene Primzahlen in der Nähe von 9176 geändert habe (während seed = 1009 verlassen wurde), und sie alle waren schlechter als die oben genannte Lösung.

Schließlich habe ich auch mit der generischen ReSharper-Empfehlungsfunktionsfamilie von hash = (hash * factor) ^ i; verglichen, und die ursprüngliche CustomHash(), wie oben erwähnt, übertrifft sie erheblich. Der ReSharper XOR - Stil scheint Kollisionsraten im Bereich von 20 bis 30% zu haben, was häufige Anwendungsfälle voraussetzt, und sollte meiner Meinung nach nicht verwendet werden.

37
Special Sauce

Wenn Sie .NET Core 2.1 verwenden, sollten Sie die Struktur System.HashCode in Betracht ziehen, um die Erstellung zusammengesetzter Hash-Codes zu unterstützen. Es gibt zwei Betriebsmodi: 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();
}

Pros:

  • Teil von .NET selbst (siehe jedoch unten)
  • Sie haben gute Performance- und Mischcharakteristika, basierend auf der Arbeit, die der Autor und die Rezensenten vor Mischen in das Corefx-Repo durchgeführt haben.
  • Verarbeitet Nullen automatisch
  • Überladungen, die IEqualityComparer -Instanzen erfordern

Nachteile:

  • Stand August 2018, nur verfügbar, wenn Sie .NET Core 2.1 als Ziel haben
    • Ich gehe davon aus, dass dies langsam in .NET Standard und dann in die verschiedenen anderen Implementierungen Einzug hält. Ich habe keine Ahnung, wann etwas davon passieren wird.
  • Universell einsetzbar, so dass es keine superspezifischen Fälle sowie handgefertigten Code behandelt
16
chwarr

Ich nehme an, das .NET Framework-Team hat seine System.String.GetHashCode () -Implementierung anständig getestet, 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 andere Implementierung ist von System.Web.Util.HashCodeCombiner.CombineHashCodes (System.Int32, System.Int32) und System.Array.CombineHashCodes (System.Int32, System.Int32) Methoden. Diese 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;
}
15
Stipo

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

(field1, field2).GetHashCode();
5
Yepeekai

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 Sie beim Prototyping eine Vorstellung von Leistung und Clustering erhalten.

0
Ed Power

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

this.toString().hashCode();

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

0
Thomas Hugel

Wenn Ihre Eingabe-Hashes die gleiche Größe haben, gleichmäßig verteilt und nicht aufeinander bezogen sind, sollte ein XOR in Ordnung sein. Und es geht schnell.

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

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

wenn zu erwarten ist, dass A und B mit einer vernünftigen (nicht vernachlässigbaren) Wahrscheinlichkeit auf den gleichen Wert gehasht werden, sollten Sie XOR auf diese Weise nicht verwenden.

0
geofftnz