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:
- XORing
- XORing mit Prime Multiplication
- Einfache numerische Operationen wie Multiplikation / Division (mit Überlaufprüfung oder Umwickeln)
- Erstellen eines Strings und anschließende Verwendung der Hash-Code-Methode der String-Klassen
Was würden die Leute empfehlen und warum?
unchecked { }
Block einschließen. GetHashCode () sollte keine Ausnahmen auslösen.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
17
und der Faktor von,31
wie 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 alsint.MaxValue
und die Anzahl der gemeinsam gehashten Elemente beträgt einige Dutzend oder weniger.Für das Hashing eines ganzzahligen Tupels
{x, y}
mit-1000 <= x <= 1000
und-1000 <= y <= 1000
hat 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 , wo3 <= 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 waren2 <= n <= 25
(won
zufä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ängigenint
undchar
Hashing-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.
1009
oben erwähnt ist in der Tat Prime, ist es aber9176
nicht. Ich habe explizit Variationen davon getestet, bei denen ichfactor
zu verschiedenen Primzahlen in der Nähe9176
(während des Verlassensseed = 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.quelle
params int[] vals
am Ende aller Funktionsargumente stehen muss, konnte ich dieseed
und die Standardparameter nicht festlegenfactor
. Wenn Sie sich nicht für dieparams
Syntaxfreundlichkeit interessieren , können Sie sie entfernen und dann die Parameter neu anordnen, um die von Ihnen vorgeschlagenen Standardeinstellungen zuzulassen.HashSet<int>
Argument akzeptieren (unter der Annahme, dass keine Duplikate vorhanden sind). Andernfalls können Sie die Funktion umbenennenCustomHashCombination()
, um Verwirrung zu vermeiden, und die interne Vorsortierung wie vorgeschlagen durchführen.params
da 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.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:
IEqualityComparer
Instanzen benötigenNachteile:
HashCode
ist Teil von .NET Standard 2.1. Ab September 2019 hat das .NET-Team keine Pläne, .NET Standard 2.1 unter .NET Framework zu unterstützen , da .NET Core / .NET 5 die Zukunft von .NET ist .quelle
Verwenden Sie die Kombinationslogik in Tupel. Das Beispiel verwendet c # 7-Tupel.
quelle
ValueTuple
Art Struktur ( MSDN ). Achten Sie darauf, dass dieserTuple
Typ 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.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; }
quelle
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
seed
undfactor
.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);
quelle
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.
quelle
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.
quelle
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.
quelle
Ich würde empfehlen, die in System.Security.Cryptography integrierten Hash-Funktionen zu verwenden, anstatt Ihre eigenen zu rollen.
quelle