Die API-Form wurde fertiggestellt. Wir entscheiden jedoch immer noch über den besten Hash-Algorithmus aus einer Liste von Kandidaten für die Implementierung, und wir brauchen jemanden, der uns hilft, den Durchsatz/die Verteilung jedes Algorithmus zu messen. Wenn Sie diese Rolle übernehmen möchten , hinterlassen Sie bitte unten einen Kommentar und
Hier ist die API, die von @terrajobst unter https://github.com/dotnet/corefx/issues/14354#issuecomment -308190321 genehmigt wurde:
// Will live in the core assembly
// .NET Framework : mscorlib
// .NET Core : System.Runtime / System.Private.CoreLib
namespace System
{
public struct HashCode
{
public static int Combine<T1>(T1 value1);
public static int Combine<T1, T2>(T1 value1, T2 value2);
public static int Combine<T1, T2, T3>(T1 value1, T2 value2, T3 value3);
public static int Combine<T1, T2, T3, T4>(T1 value1, T2 value2, T3 value3, T4 value4);
public static int Combine<T1, T2, T3, T4, T5>(T1 value1, T2 value2, T3 value3, T4 value4, T5 value5);
public static int Combine<T1, T2, T3, T4, T5, T6>(T1 value1, T2 value2, T3 value3, T4 value4, T5 value5, T6 value6);
public static int Combine<T1, T2, T3, T4, T5, T6, T7>(T1 value1, T2 value2, T3 value3, T4 value4, T5 value5, T6 value6, T7 value7);
public static int Combine<T1, T2, T3, T4, T5, T6, T7, T8>(T1 value1, T2 value2, T3 value3, T4 value4, T5 value5, T6 value6, T7 value7, T8 value8);
public void Add<T>(T value);
public void Add<T>(T value, IEqualityComparer<T> comparer);
[Obsolete("Use ToHashCode to retrieve the computed hash code.", error: true)]
[EditorBrowsable(Never)]
public override int GetHashCode();
public int ToHashCode();
}
}
Der Originaltext dieses Vorschlags folgt.
Das Generieren eines guten Hash-Codes sollte nicht die Verwendung hässlicher magischer Konstanten und das Herumspielen unseres Codes erfordern. Es sollte weniger verlockend sein, eine schlechte, aber prägnante GetHashCode
Implementierung zu schreiben, wie zum Beispiel
class Person
{
public override int GetHashCode() => FirstName.GetHashCode() + LastName.GetHashCode();
}
Wir sollten einen HashCode
Typ hinzufügen, um die Hash-Code-Erstellung zu kapseln und zu vermeiden, dass Entwickler gezwungen werden, sich in die unordentlichen Details zu verwickeln. Hier ist mein Vorschlag, der auf https://github.com/dotnet/corefx/issues/14354#issuecomment -305019329 basiert, mit einigen kleineren Überarbeitungen.
// Will live in the core assembly
// .NET Framework : mscorlib
// .NET Core : System.Runtime / System.Private.CoreLib
namespace System
{
public struct HashCode
{
public static int Combine<T1>(T1 value1);
public static int Combine<T1, T2>(T1 value1, T2 value2);
public static int Combine<T1, T2, T3>(T1 value1, T2 value2, T3 value3);
public static int Combine<T1, T2, T3, T4>(T1 value1, T2 value2, T3 value3, T4 value4);
public static int Combine<T1, T2, T3, T4, T5>(T1 value1, T2 value2, T3 value3, T4 value4, T5 value5);
public static int Combine<T1, T2, T3, T4, T5, T6>(T1 value1, T2 value2, T3 value3, T4 value4, T5 value5, T6 value6);
public static int Combine<T1, T2, T3, T4, T5, T6, T7>(T1 value1, T2 value2, T3 value3, T4 value4, T5 value5, T6 value6, T7 value7);
public static int Combine<T1, T2, T3, T4, T5, T6, T7, T8>(T1 value1, T2 value2, T3 value3, T4 value4, T5 value5, T6 value6, T7 value7, T8 value8);
public void Add<T>(T value);
public void Add<T>(T value, IEqualityComparer<T> comparer);
public void AddRange<T>(T[] values);
public void AddRange<T>(T[] values, int index, int count);
public void AddRange<T>(T[] values, int index, int count, IEqualityComparer<T> comparer);
[Obsolete("Use ToHashCode to retrieve the computed hash code.", error: true)]
public override int GetHashCode();
public int ToHashCode();
}
}
Siehe @terrajobst ‚s Kommentar zu https://github.com/dotnet/corefx/issues/14354#issuecomment -305.019.329 für die Ziele dieser API; alle seine Bemerkungen sind gültig. Auf diese möchte ich aber besonders hinweisen:
Vorschlag: Unterstützung für Hash-Randomisierung hinzufügen
public static HashCode Randomized<T> { get; } // or CreateRandomized<T>
or
public static HashCode Randomized(Type type); // or CreateRandomized(Type type)
T
oder Type type
wird benötigt, um denselben randomisierten Hash für denselben Typ zu erhalten.
Vorschlag: Unterstützung für Sammlungen hinzufügen
public HashCode Combine<T>(T[] values);
public HashCode Combine<T>(T[] values, IEqualityComparer<T> comparer);
public HashCode Combine<T>(Span<T> values);
public HashCode Combine<T>(Span<T> values, IEqualityComparer<T> comparer);
public HashCode Combine<T>(IEnumerable<T> values);
public HashCode Combine<T>(IEnumerable<T> IEqualityComparer<T> comparer);
Ich denke, es besteht keine Notwendigkeit für Überladungen Combine(_field1, _field2, _field3, _field4, _field5)
da der nächste Code HashCode.Empty.Combine(_field1).Combine(_field2).Combine(_field3).Combine(_field4).Combine(_field5);
ohne Combine-Aufrufe inline optimiert werden sollte.
@AlexRadch
Vorschlag: Unterstützung für Sammlungen hinzufügen
Ja, das war Teil meines späteren Plans für diesen Vorschlag. Ich denke, es ist jedoch wichtig, sich darauf zu konzentrieren, wie die API aussehen soll, bevor wir diese Methoden hinzufügen.
Er wollte einen anderen Algorithmus verwenden, wie den Marvin32-Hash, der für Strings in coreclr verwendet wird. Dies würde eine Erweiterung der Größe von HashCode auf 8 Bytes erfordern.
Wie wäre es mit Hash32- und Hash64-Typen, die intern 4 oder 8 Byte Daten speichern würden? Dokumentieren Sie die Vor- und Nachteile jedes einzelnen. Hash64 ist gut für X, aber potenziell langsamer. Hash32 ist schneller, aber möglicherweise nicht so verteilt (oder was auch immer der Kompromiss tatsächlich ist).
Er wollte den Hash-Seed randomisieren, damit Hashes nicht deterministisch sind.
Dies scheint ein nützliches Verhalten zu sein. Aber ich konnte sehen, dass Leute das kontrollieren wollten. Vielleicht sollte es also zwei Möglichkeiten geben, den Hash zu erstellen, eine, die keinen Seed benötigt (und einen zufälligen Seed verwendet) und eine, die die Bereitstellung des Seeds ermöglicht.
Hinweis: Roslyn würde sich freuen, wenn dies in der Fx bereitgestellt werden könnte. Wir fügen eine Funktion hinzu, um einen GetHashCode für den Benutzer auszuspucken. Derzeit generiert es Code wie:
c#
public override int GetHashCode()
{
var hashCode = -1923861349;
hashCode = hashCode * -1521134295 + this.b.GetHashCode();
hashCode = hashCode * -1521134295 + this.i.GetHashCode();
hashCode = hashCode * -1521134295 + EqualityComparer<string>.Default.GetHashCode(this.s);
return hashCode;
}
Dies ist keine großartige Erfahrung und enthüllt viele hässliche Konzepte. Wir würden uns freuen, eine Hash.Whatever-API zu haben, die wir stattdessen aufrufen könnten.
Vielen Dank!
Was ist mit MurmurHash? Es ist relativ schnell und hat sehr gute Hashing-Eigenschaften. Es gibt auch zwei verschiedene Implementierungen, eine, die 32-Bit-Hashes ausspuckt, und eine andere, die 128-Bit-Hashes ausspuckt.
Es gibt auch vektorisierte Implementierungen sowohl für das 32-Bit- als auch für das 128-Bit-Format.
@tannergooding MurmurHash ist schnell, aber nicht sicher, von den Klängen dieses Blogbeitrags .
@jkotas , gab es im JIT seit unseren Diskussionen im letzten Jahr irgendwelche Arbeiten zur Generierung von besserem Code für > 4-Byte-Strukturen auf 32-Bit? Und was halten Sie von
Wie wäre es mit Hash32- und Hash64-Typen, die intern 4 oder 8 Byte Daten speichern würden? Dokumentieren Sie die Vor- und Nachteile jedes einzelnen. Hash64 ist gut für X, aber potenziell langsamer. Hash32 ist schneller, aber möglicherweise nicht so verteilt (oder was auch immer der Kompromiss tatsächlich ist).
Ich denke immer noch, dass es sehr wertvoll wäre, diesen Typ Entwicklern anzubieten, und es wäre großartig, ihn in 2.0 zu haben.
@jamesqo , ich glaube nicht, dass diese Implementierung kryptografisch sicher sein muss (das ist der Zweck der expliziten kryptografischen Hashing-Funktionen).
Dieser Artikel gilt auch für Murmur2. Das Problem wurde im Murmur3-Algorithmus behoben.
das JIT um besseren Code für >4-Byte-Strukturen auf 32-Bit zu generieren seit unseren Diskussionen im letzten Jahr
Mir sind keine bekannt.
Was hältst du von @CyrusNajmabadis Vorschlag
Die Framework-Typen sollten einfache Entscheidungen sein, die in über 95 % der Fälle gut funktionieren. Sie sind vielleicht nicht die schnellsten, aber das ist in Ordnung. Die Wahl zwischen Hash32 und Hash64 ist keine einfache Wahl.
Das ist okay für mich. Aber können wir für diese 95%-Fälle zumindest eine ausreichend gute Lösung haben? Im Moment ist nichts... :-/
hashCode = hashCode * -1521134295 + EqualityComparer
.Default.GetHashCode(this.s);
@CyrusNajmabadi Warum
Für Nicht-Structs: damit wir nicht auf null prüfen müssen.
Dies kommt dem nahe, was wir auch für anonyme Typen hinter den Kulissen generieren. Ich optimiere den Fall bekannter Nicht-Null-Werte, um Code zu generieren, der für die Benutzer angenehmer wäre. Aber es wäre schön, dafür nur eine eingebaute API zu haben.
Der Aufruf von EqualityComparer.Default.GetHashCode ist ungefähr 10x teurer als die Prüfung auf null... .
Der Aufruf von EqualityComparer.Default.GetHashCode ist ungefähr 10x teurer als die Prüfung auf null.
Klingt nach einem Problem. Wenn es nur eine gute Hash-Code-API gäbe, könnten wir die Fx aufrufen, auf die ich mich verschieben könnte :)
(Außerdem haben wir dieses Problem dann in unseren anonymen Typen, da wir das auch dort generieren).
Ich bin mir nicht sicher, was wir für Tupel tun, aber ich vermute, es ist ähnlich.
Ich bin mir nicht sicher, was wir für Tupel tun, aber ich vermute, es ist ähnlich.
System.Tuple
geht aus historischen Gründen durch EqualityComparer<Object>.Default
. System.ValueTuple
ruft Object.GetHashCode mit Nullprüfung auf - https://github.com/dotnet/coreclr/blob/master/src/mscorlib/shared/System/ValueTuple.cs#L809.
Ach nein. Sieht so aus, als ob Tupel einfach "HashHelpers" verwenden kann. Könnte das aufgedeckt werden, damit die Benutzer den gleichen Nutzen haben?
Groß. Ich mache gerne etwas Ähnliches. Ich habe mit unseren anonymen Typen angefangen, weil ich dachte, dass sie vernünftige Best Practices sind. Wenn nicht, ist das in Ordnung. :)
Aber deswegen bin ich nicht hier. Ich bin hier, um ein System zu bekommen, das die Hashes effektiv kombiniert. Wenn/wenn das bereitgestellt werden kann, gehen wir gerne dazu über, das aufzurufen, anstatt in Zufallszahlen fest zu codieren und Hash-Werte selbst zu kombinieren.
Welche API-Form wäre Ihrer Meinung nach am besten für den vom Compiler generierten Code geeignet?
Buchstäblich jede der früher vorgestellten 32-Bit-Lösungen wäre für mich in Ordnung. Verdammt, 64bit-Lösungen sind für mich in Ordnung. Nur eine Art API, die Sie erhalten können, die besagt: "Ich kann Hashes auf eine vernünftige Weise kombinieren und ein vernünftig verteiltes Ergebnis erzeugen".
Ich kann diese Aussagen nicht vereinen:
Wir hatten eine unveränderliche HashCode-Struktur mit einer Größe von 4 Byte. Es hatte eine Combine(int)-Methode, die über einen DJBX33X-ähnlichen Algorithmus den bereitgestellten Hash-Code mit seinem eigenen Hash-Code vermischte und einen neuen HashCode zurückgab.
@jkotas hielt den DJBX33X-ähnlichen Algorithmus nicht für robust genug.
Und
Die Framework-Typen sollten einfache Entscheidungen sein, die in über 95 % der Fälle gut funktionieren.
Können wir nicht einen einfachen 32-Bit-akkumulierenden Hash entwickeln, der in 95% der Fälle gut genug funktioniert? Was sind die Fälle, die hier nicht gut gehandhabt werden, und warum sind sie unserer Meinung nach im Fall von 95 %?
@jkotas , ist die Leistung für diesen Typ wirklich so dies würde viel mehr Zeit als ein paar struct Kopien aufzunehmen. Wenn es sich als Engpass herausstellt, wäre es vernünftig, das JIT-Team zu bitten, 32-Bit-Strukturkopien nach der Veröffentlichung der API zu optimieren, damit sie einen Anreiz haben, anstatt diese API zu blockieren, wenn niemand an der Optimierung arbeitet Kopien?
Können wir nicht einen einfachen 32-Bit-akkumulierenden Hash entwickeln, der in 95% der Fälle gut genug funktioniert?
Wir haben standardmäßig 32-Bit-Hash für Strings akkumulieren wirklich schlecht gebrannt, und deshalb Marvin-Hash für Strings in .NET Core - https://github.com/dotnet/corert/blob/87e58839d6629b5f90777f886a2f52d7a99c076f/src/System.Private.CoreLib/ src/System/Marvin.cs#L25. Ich glaube nicht, dass wir hier denselben Fehler wiederholen wollen.
@jkotas , ist die Leistung für diesen Typ wirklich so
Ich denke nicht, dass die Leistung entscheidend ist. Da es so aussieht, als ob diese API von automatisch generiertem Compilercode verwendet wird, denke ich, dass wir kleineren generierten Code dem Aussehen vorziehen sollten. Das nicht fließende Muster ist kleinerer Code.
Wir wurden wirklich schlecht gebrannt, standardmäßig 32-Bit, das Hash für String ansammelt
Das scheint nicht der 95%-Fall zu sein. Wir sprechen von normalen Entwicklern, die nur einen "gut genug" Hash für all die Typen wollen, bei denen sie heute Dinge manuell tun.
Da es so aussieht, als ob diese API von automatisch generiertem Compilercode verwendet wird, denke ich, dass wir kleineren generierten Code dem Aussehen vorziehen sollten. Das nicht fließende Muster ist kleinerer Code.
Dies ist nicht für den Roslyn-Compiler vorgesehen. Dies wird von der Roslyn-IDE verwendet, wenn wir Benutzern helfen, GetHashCodes für ihre Typen zu generieren. Dies ist Code, den der Benutzer sehen und pflegen muss und der etwas Vernünftiges hat wie:
```c#
return Hash.Combine(this.A?.GetHashCode() ?? 0,
this.B?.GetHashCode() ?? 0,
this.C?.GetHashCode() ?? 0);
is a lot nicer than a user seeing and having to maintain:
```c#
var hashCode = -1923861349;
hashCode = hashCode * -1521134295 + this.b.GetHashCode();
hashCode = hashCode * -1521134295 + this.i.GetHashCode();
hashCode = hashCode * -1521134295 + EqualityComparer<string>.Default.GetHashCode(this.s);
return hashCode;
Ich meine, wir haben diesen Code bereits im Fx:
Wir denken, es ist gut genug für Tupel. Es ist mir unklar, warum es ein solches Problem sein sollte, es Benutzern zur Verfügung zu stellen, die es für ihre eigenen Typen haben möchten.
Hinweis: Wir haben sogar in Erwägung gezogen, dies in Roslyn zu tun:
c#
return (this.A, this.B, this.C).GetHashCode();
Aber jetzt zwingen Sie die Leute, eine (potenziell große) Struktur zu generieren, nur um ein vernünftiges Standard-Hashing-Verhalten zu erhalten.
Wir sprechen von normalen Entwicklern, die nur einen "gut genug" Hash für all die Typen wollen, bei denen sie heute Dinge manuell tun.
Der ursprüngliche String-Hash war ein "gut genug" Hash, der für normale Entwickler gut funktionierte. Aber dann wurde entdeckt, dass ASP.NET-Webserver anfällig für DoS-Angriffe waren, da sie dazu neigen, empfangene Daten in Hashtables zu speichern. So wurde der "gut genug" Hash im Grunde zu einem schlechten Sicherheitsproblem.
Wir denken, es ist gut genug für Tupel
Nein unbedingt. Wir haben eine Backstop-Messung für Tupel gemacht, um den Hashcode randomisiert zu machen, der uns die Möglichkeit gibt, den Algorithmus später zu ändern.
return Hash.Combine(this.A?.GetHashCode() ?? 0, this.B?.GetHashCode() ?? 0, this.C?.GetHashCode() ?? 0);
Das sieht für mich vernünftig aus.
Ich verstehe Ihre Position nicht. Du scheinst zwei Dinge zu sagen:
Der ursprüngliche String-Hash war ein "gut genug" Hash, der für normale Entwickler gut funktionierte. Aber dann wurde entdeckt, dass ASP.NET-Webserver anfällig für DoS-Angriffe waren, da sie dazu neigen, empfangene Daten in Hashtables zu speichern. So wurde der "gut genug" Hash im Grunde zu einem schlechten Sicherheitsproblem.
Ok, wenn das der Fall ist, dann stellen wir einen Hash-Code bereit, der gut für Leute ist, die Sicherheits-/DoS-Bedenken haben.
Die Framework-Typen sollten einfache Entscheidungen sein, die in über 95 % der Fälle gut funktionieren.
Ok, wenn das der Fall ist, stellen wir einen Hash-Code bereit, der für die 95% der Fälle gut genug ist. Personen, die Sicherheits-/DoS-Bedenken haben, können die zu diesem Zweck dokumentierten speziellen Formulare verwenden.
Nein unbedingt. Wir haben eine Backstop-Messung für Tupel gemacht, um den Hashcode randomisiert zu machen, der uns die Möglichkeit gibt, den Algorithmus später zu ändern.
Okay. Können wir das offenlegen, damit Benutzer denselben Mechanismus verwenden können?
--
Ich habe hier wirklich Probleme, weil es so klingt, als würden wir sagen "weil wir keine universelle Lösung finden können, muss jeder seine eigene rollen". Das scheint einer der schlimmsten Orte zu sein. Denn sicherlich denken die meisten unserer Kunden nicht daran, ihren eigenen "Marvin-Hash" für DoS-Bedenken zu rollen. Sie fügen nur Feld-Hashes hinzu, xoring oder kombiniert sie auf andere Weise schlecht zu einem endgültigen Hash.
Wenn uns der Fall von 95 % wichtig ist, sollten wir einfach einen allgemein guten Hasch herstellen. WENN uns der 5%-Fall wichtig ist, können wir dafür eine spezialisierte Lösung liefern.
Das sieht für mich vernünftig aus.
Toll :) Können wir dann aussetzen:
```c#
Namespace System.Numerics.Hashing
{
interne statische Klasse HashHelpers
{
public static readonly int RandomSeed = new Random().Next(Int32.MinValue, Int32.MaxValue);
public static int Combine(int h1, int h2)
{
// RyuJIT optimizes this to use the ROL instruction
// Related GitHub pull request: dotnet/coreclr#1830
uint rol5 = ((uint)h1 << 5) | ((uint)h1 >> 27);
return ((int)rol5 + h1) ^ h2;
}
}
Roslyn could then generate:
```c#
return Hash.Combine(Hash.RandomSeed,
this.A?.GetHashCode() ?? 0,
this.B?.GetHashCode() ?? 0,
this.C?.GetHashCode() ?? 0);
Dies hätte den Vorteil, dass es für die überwiegende Mehrheit der Fälle wirklich "gut genug" ist, während es gleichzeitig den guten Weg der Initialisierung mit zufälligen Werten führt, damit sie keine Abhängigkeiten von nicht zufälligen Hashes eingehen.
Personen, die Sicherheits-/DoS-Bedenken haben, können die zu diesem Zweck dokumentierten speziellen Formulare verwenden.
Jede ASP.NET-App hat Sicherheits-/DoS-Bedenken.
Toll :) Können wir dann aussetzen:
Dies unterscheidet sich von dem, was ich gesagt habe, ist vernünftig.
Was halten Sie von https://github.com/aspnet/Common/blob/dev/shared/Microsoft.Extensions.HashCodeCombiner.Sources/HashCodeCombiner.cs . Es ist das, was heute in ASP.NET intern an vielen Stellen verwendet wird, und damit wäre ich ziemlich zufrieden (außer dass die Kombinationsfunktion stärker sein muss - Implementierungsdetails, die wir weiter optimieren können).
@jkotas Ich habe das gehört :p
Das Problem hier ist also, dass Entwickler nicht wissen, wann sie anfällig für DoS-Angriffe sind, weil sie nicht darauf achten, weshalb wir die Strings auf Marvin32 umgestellt haben.
Wir sollten nicht den Weg einschlagen, zu sagen "95 % der Fälle spielen keine Rolle", denn wir haben keine Möglichkeit, dies zu beweisen, und wir müssen auf der Seite der Vorsicht sein, selbst wenn dies Leistungskosten verursacht. Wenn Sie sich davon entfernen möchten, muss die Hash-Code-Implementierung vom Crypto Board überprüft werden, und nicht nur wir entscheiden "Das sieht gut genug aus".
Jede ASP.NET-App hat Sicherheits-/DoS-Bedenken.
Okay. Wie gehen Sie heute mit dem Problem um, dass niemand Hilfe bei Hashcodes hat und daher wahrscheinlich die Dinge schlecht macht? Es war eindeutig akzeptabel, diesen Zustand der Welt zu haben. Was schadet also, wenn ein vernünftiges Hashing-System bereitgestellt wird, das wahrscheinlich besser funktioniert als das, was die Leute heute von Hand rollen?
weil wir das nicht beweisen können, und wir müssen auf der sicheren Seite sein, selbst wenn es Leistungskosten hat
Wenn Sie etwas nicht bereitstellen, werden die Leute weiterhin Dinge schlecht machen. Die Ablehnung des "Guten", weil es nichts Perfektes gibt, bedeutet nur den schlechten Status quo, den wir heute haben.
Jede ASP.NET-App hat Sicherheits-/DoS-Bedenken.
Können Sie das erklären? Wie ich es verstehe, haben Sie ein DoS-Problem, wenn Sie beliebige Eingaben akzeptieren und diese dann in einer Datenstruktur speichern, die schlecht funktioniert, wenn die Eingaben speziell gestaltet werden können. Ok, ich verstehe, dass das ein Problem mit den Zeichenfolgen ist, die man in Web-Szenarien erhält, die vom Benutzer stammen.
Wie trifft das auf den Rest der Typen zu, die in diesem Szenario nicht verwendet werden?
Wir haben diese Arten von Sets:
Grundsätzlich halten wir diese Fälle für wichtig, aber nicht wichtig genug, um den Benutzern tatsächlich eine Lösung für den Umgang mit '1' oder '2' zu bieten. Da wir befürchten, dass eine Lösung für '2' nicht gut für '1' ist, werden wir sie erst gar nicht bereitstellen. Und wenn wir nicht einmal bereit sind, eine Lösung für '1' anzubieten, fühlt es sich an, als wären wir in einer unglaublich seltsamen Position. Wir machen uns Sorgen um DoSing und ASP, aber nicht genug, um Menschen tatsächlich zu helfen. Und weil wir den Leuten damit nicht helfen, sind wir auch nicht bereit, bei den Nicht-DoS-Fällen zu helfen.
--
Wenn diese beiden Fälle wichtig sind (was ich gerne akzeptieren möchte), warum dann nicht einfach zwei APIs geben? Dokumentieren Sie sie. Machen Sie ihnen klar, wozu sie dienen. Wenn die Leute sie richtig verwenden, großartig . Wenn die Leute sie nicht richtig verwenden, ist das immer noch in Ordnung. Schließlich machen sie die Dinge heute wahrscheinlich sowieso nicht richtig, also wie geht es ihnen noch schlimmer?
Was denkst du über
Ich habe keine Meinung so oder so. Wenn es sich um eine API handelt, die Kunden verwenden können, die eine akzeptable Leistung bietet und die eine einfache API mit klarem Code an ihrer Seite bereitstellt, dann ist das meiner Meinung nach in Ordnung.
Ich denke, es wäre schön, ein einfaches statisches Formular zu haben, das den 99%igen Fall behandelt, einen Satz von Feldern/Eigenschaften in einer geordneten Weise kombinieren zu wollen. Es scheint, als könnte so etwas ziemlich einfach zu diesem Typ hinzugefügt werden.
Ich denke, es wäre schön, eine einfache statische Form zu haben
Zustimmen.
Ich denke, es wäre schön, ein einfaches statisches Formular zu haben, das den 99%igen Fall behandelt, einen Satz von Feldern/Eigenschaften in einer geordneten Weise kombinieren zu wollen. Es scheint, als könnte so etwas ziemlich einfach zu diesem Typ hinzugefügt werden.
Zustimmen.
Ich bin bereit, Sie beide auf halbem Weg zu treffen, weil ich wirklich möchte, dass eine Art API durchkommt. @jkotas Ich verstehe immer noch nicht, dass Sie dagegen sind, eine unveränderliche h.Combine(a).Combine(b)
(unveränderliche Version) ist kürzer als h.Combine(a); h.Combine(b);
(veränderlich .) Ausführung)).
Das heißt, ich bin bereit, zurück zu gehen:
public static class HashCode
{
public static int Combine<T>(T value1, Tvalue2);
public static int Combine<T>(T value1, Tvalue2, IEqualityComparer<T> comparer);
public static int Combine<T>(T value1, Tvalue2, T value3);
public static int Combine<T>(T value1, Tvalue2, T value3, IEqualityComparer<T> comparer);
public static int Combine<T>(T value1, Tvalue2, T value3, T value4);
public static int Combine<T>(T value1, Tvalue2, T value3, T value4, IEqualityComparer<T> comparer);
// ... All the way until value8
}
Erscheint dies vernünftig?
Ich kann meinen Beitrag gerade nicht bearbeiten, aber ich habe gerade festgestellt, dass nicht alle Methoden T akzeptieren können. In diesem Fall können wir einfach 8 Überladungen haben, die alle Ints akzeptieren und den Benutzer zwingen, GetHashCode aufzurufen.
Wenn diese beiden Fälle wichtig sind (was ich gerne akzeptieren möchte), warum dann nicht einfach zwei APIs geben? Dokumentieren Sie sie. Machen Sie ihnen klar, wozu sie dienen. Wenn die Leute sie richtig verwenden, großartig. Wenn die Leute sie nicht richtig verwenden, ist das immer noch in Ordnung. Schließlich machen sie die Dinge heute wahrscheinlich sowieso nicht richtig, also wie geht es ihnen noch schlimmer?
Weil die Leute die Dinge nicht richtig benutzen, wenn sie dort sind. Nehmen wir ein einfaches Beispiel, XSS. Von Anfang an hatten sogar Webformulare die Möglichkeit, die Ausgabe in HTML zu kodieren. Allerdings kannten die Entwickler das Risiko nicht, wussten nicht, wie man es richtig macht, und fanden es erst heraus, als es zu spät war, ihre App veröffentlicht wurde und oops, jetzt wurde ihr Authentifizierungs-Cookie aufgehoben.
Den Menschen eine Sicherheitswahl zu geben, setzt voraus, dass sie
Diese Annahmen gelten im Allgemeinen nicht für die Mehrheit der Entwickler, sie erfahren das Problem erst, wenn es zu spät ist. Entwickler gehen nicht zu Sicherheitskonferenzen, lesen keine Whitepaper und verstehen die Lösungen nicht. Also haben wir im ASP.NET HashDoS-Szenario die Wahl für sie getroffen, wir haben sie standardmäßig geschützt, weil das richtig war und die größte Wirkung hatte. Wir haben es jedoch nur auf Strings angewendet, und das führte dazu, dass Leute, die benutzerdefinierte Klassen aus Benutzereingaben erstellten, an einem schlechten Ort waren. Wir sollten das Richtige tun und helfen, diese Kunden jetzt zu schützen und es zum Standard zu machen, um Erfolg und nicht Misserfolg zu erzielen. Beim API-Design für die Sicherheit geht es manchmal nicht um die Wahl, sondern darum, dem Benutzer zu helfen, ob er es weiß oder nicht.
Ein Benutzer kann jederzeit einen nicht sicherheitsorientierten Hash erstellen; also die zwei möglichkeiten gegeben
Dann ist die zweite wahrscheinlich besser; und was vorgeschlagen wird, hätte nicht die perfekte Auswirkung eines vollständigen Krypto-Hashs; also ist es ein guter kompromiss?
Eine der ständigen Fragen in diesen Threads war, welcher Algorithmus für jeden perfekt ist. Ich denke, man kann mit Sicherheit sagen, dass es keinen einzigen perfekten Algorithmus gibt. Ich glaube jedoch nicht, dass uns das davon abhalten sollte, etwas Besseres als Code @CyrusNajmabadi gezeigt hat, der dazu neigt, eine schlechte Entropie für allgemeine .NET-Eingaben sowie andere häufige Hasher-Bugs (wie das Verlieren von Eingabedaten oder leichtes Arbeiten) zu haben rücksetzbar).
Ich möchte ein paar Optionen vorschlagen, um das Problem des "besten Algorithmus" zu umgehen:
Marvin32.Create();
generiert, können erfahrene Benutzer wissen, was sie tun, und sie können ihn bei Bedarf problemlos auf einen anderen Algorithmus in der Suite ändern.CC @bartonjs , @terrajobst
@morganbr Es gibt keinen einzigen perfekten Algorithmus, aber ich denke, dass es das verfügbar gemacht wird. Zu haben , ist eine Reihe von Algorithmen zusätzlich zu , dass für fortgeschrittene Anwendungen in Ordnung. Aber es sollte nicht die einzige Option sein, ich sollte nicht lernen müssen, wer Marvin ist, nur damit ich meine Objekte in ein Dictionary
.
Ich sollte nicht wissen müssen, wer Marvin ist, nur damit ich meine Objekte in ein Wörterbuch eintragen kann.
Ich mag die Art, wie du das ausdrückst. Mir gefällt auch, dass Sie das Wörterbuch selbst erwähnt haben. IDictionary ist etwas, das Tonnen von verschiedenen Impls mit allen möglichen unterschiedlichen Qualitäten haben kann (siehe die Sammlungs-APIs in vielen Plattformen). Wir bieten jedoch immer noch nur ein grundlegendes 'Wörterbuch', das insgesamt eine anständige Arbeit leistet, auch wenn es möglicherweise nicht in jeder Kategorie hervorragend ist.
Ich denke, das ist es, wonach eine Menge Leute in einer Hashing-Bibliothek suchen. Etwas, das die Arbeit erledigt, auch wenn es nicht für jeden Zweck perfekt ist.
@morganbr Ich denke, die Leute wollen einfach eine Möglichkeit, GetHashCode zu schreiben, der besser ist als das, was sie heute tun (normalerweise eine Kombination aus mathematischen Operationen, die sie aus dem Internet kopiert haben). Wenn Sie nur ein grundlegendes Impl davon bereitstellen können, das gut läuft, werden die Leute glücklich sein. Sie können dann eine API hinter den Kulissen für fortgeschrittene Benutzer haben, wenn sie einen starken Bedarf an bestimmten Hashing-Funktionen haben.
Mit anderen Worten, die Leute, die heute Hashcodes schreiben, werden nicht wissen oder sich darum kümmern, warum sie Spooky vs Marvin vs Murmur wollen. Nur jemand, der einen bestimmten Bedarf an einem dieser spezifischen Hash-Codes hat, würde suchen. Aber viele Leute müssen sagen: "Hier ist der Zustand meines Objekts, geben Sie mir eine Möglichkeit, einen gut verteilten Hash zu erzeugen, der schnell ist, den ich dann mit Wörterbüchern verwenden kann und der mich vermutlich daran hindert, DOSiert zu werden, wenn ich dazu komme nicht vertrauenswürdige Eingaben zu nehmen, zu hashen und zu speichern".
@CyrusNajmabadi Das Problem ist, dass wir, wenn wir unsere aktuellen Vorstellungen von Kompatibilität in die Zukunft
Once kann argumentieren, dass es einfach wird, die Implementierung zu ändern, wenn es als stabil-randomisierte Methode beginnt, da Sie sich sowieso nicht auf den Wert von Lauf zu Lauf verlassen können. Aber wenn wir ein paar Jahre später feststellen, dass es einen Algorithmus gibt, der einen so guten, wenn nicht sogar besseren Ausgleich von Hash-Buckets mit einer im Allgemeinen besseren Leistung bietet, aber eine Struktur mit einer Liste erstellt\ Nach Morgans Vorschlag wird der Code, den Sie heute schreiben, für immer dieselben Leistungsmerkmale aufweisen. Für die Anwendungen, die besser hätten werden können, ist dies bedauerlich. Für die Anwendungen, die schlimmer geworden wären, ist das fantastisch. Aber wenn wir den neuen Algorithmus finden, bekommen wir ihn eingecheckt, und wir ändern Roslyn (und schlagen eine Änderung in ReSharper/etc vor), um Dinge mit NewAwesomeThing2019 anstelle von SomeThingThatWasConsideredAwesomeIn2018 zu generieren. So etwas wie diese Super-Blackbox kann man nur einmal machen. Und dann bleiben wir für immer dabei. Dann schreibt jemand die nächste, die eine bessere durchschnittliche Leistung hat, also gibt es zwei Black-Box-Implementierungen, von denen Sie nicht wissen, warum Sie sich zwischen ihnen entscheiden sollten. Und dann... und dann... . Sicher, Sie wissen vielleicht nicht, warum Roslyn/ReSharper/etc GetHashCode automatisch für Sie geschrieben hat, indem sie Marvin32 oder Murmur oder FastHash oder eine Kombination/Bedingung basierend auf IntPtr.Size verwendet. Aber Sie haben die Macht, es zu untersuchen. Und Sie haben die Möglichkeit, es später an Ihren Typen zu ändern, wenn neue Informationen bekannt werden ... aber wir haben Ihnen auch die Möglichkeit gegeben, es gleich zu lassen. (Es wäre traurig, wenn wir dies schreiben würden, und in 3 Jahren vermeiden Roslyn/ReSharper/etc explizit, es zu nennen, weil der neue Algorithmus so viel besser ist ... Normalerweise).
@bartonjs Was unterscheidet Hashing von all den Orten, an denen .Net einen Black-Box-Algorithmus oder eine Datenstruktur bereitstellt? Zum Beispiel Sortierung (Introsort), Dictionary
(Array-basierte separate Verkettung), StringBuilder
(verknüpfte Liste mit 8k Chunks), die meisten von LINQ.
Wir haben uns das heute genauer angeschaut. Entschuldigung für die Verzögerung und das Hin und Her zu diesem Thema.
```C#
// Wird in der Kernbaugruppe leben
// .NET Framework: mscorlib
// .NET Core: System.Runtime / System.Private.CoreLib
Namensraum-System
{
öffentliche Struktur HashCode
{
public static int Kombinieren
public static int Kombinieren
public static int Kombinieren
public static int Kombinieren
public static int Kombinieren
public static int Kombinieren
public static int Kombinieren
public static int Kombinieren
public void Add<T>(T value);
public void Add<T>(T value, IEqualityComparer<T> comparer);
public void Add<T>(T[] value);
public void Add<T>(T[] value, int index, int length);
public void Add(byte[] value);
public void Add(byte[] value, int index, int length);
public void Add(string value);
public void Add(string value, StringComparison comparisonType);
public int ToHashCode();
}
}
Notes:
* We decided to not override `GetHashCode()` to produce the hash code as this would be weird, both naming-wise as well as from a behavioral standpoint (`GetHashCode()` should return the object's hash code, not the one being computed).
* We decided to use `Add` for the builder patter and `Combine` for the static construction
* We decided to use not provide a static initialization method. Instead, `Add` will do this on first use.
* The struct is mutable, which is unfortunate but we feel the best compromise between making `GetHashCode()` very cheap & not cause any allocations while allowing the structure to be bigger than 32-bit so that the hash code algorithm can use more bits during accumulation.
* `Combine` will just call `<value>.GetHashCode()`, so it has the behavior of the value's type `GetHashCode()` implementation
- For strings that means different casing will produce different hash codes
- For arrays, that means the hash code doesn't look at the contents but uses reference semantics for the hash code
- If that behavior is undesired, the developer needs to use the builder-style approach
### Usage
The simple case is when someone just wants to produce a good hash code for a given type, like so:
```C#
public class Customer
{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public override int GetHashCode() => HashCode.Combine(Id, FirstName, LastName);
}
Der kompliziertere Fall ist, wenn der Entwickler die Berechnung des Hashs optimieren muss. Die Idee ist, dass die Aufrufseite den gewünschten Hash und nicht das Objekt / den Wert übergibt, wie folgt:
```C#
öffentliche Teilklasse Kunde
{
öffentliche Überschreibung int GetHashCode() =>
HashCode.Combine(
Ausweis,
StringComparer.OrdinalIgnoreCase.GetHashCode(FirstName),
StringComparer.OrdinalIgnoreCase.GetHashCode(Nachname),
);
}
And lastly, if the developer needs more flexibility, such as producing a hash code for more than eight values, we also provide a builder-style approach:
```C#
public partial class Customer
{
public override int GetHashCode()
{
var hashCode = new HashCode();
hashCode.Add(Id);
hashCode.Add(FirstName, StringComparison.OrdinalIgnoreCase);
hashCode.Add(LastName, StringComparison.OrdinalIgnoreCase);
return hashCode.ToHashCode();
}
}
Dieses Thema wird weiterhin offen bleiben. Um die API zu implementieren, müssen wir entscheiden, welcher Algorithmus verwendet werden soll.
@morganbr macht einen Vorschlag für gute Kandidaten. Im Allgemeinen möchten wir keinen Hashing-Algorithmus von Grund auf neu schreiben – wir möchten einen bekannten verwenden, dessen Eigenschaften gut verstanden sind.
Wir sollten jedoch die Implementierung für typische .NET-Workloads messen und sehen, welcher Algorithmus gute Ergebnisse liefert (Durchsatz und Verteilung). Es ist wahrscheinlich, dass sich die Antworten je nach CPU-Architektur unterscheiden, daher sollten wir dies bei der Messung berücksichtigen.
@jamesqo , hast du noch Interesse in diesem Bereich zu arbeiten? Bitte aktualisieren Sie in diesem Fall das Angebot entsprechend.
@terrajobst , wir möchten vielleicht auch public static int Combine<T1>(T1 value);
. Ich weiß, es sieht ein bisschen komisch aus, aber es würde eine Möglichkeit bieten, Bits von etwas mit einem begrenzten Eingabe-Hash-Raum zu diffundieren. Viele Enumerationen haben beispielsweise nur wenige mögliche Hashes und verwenden nur die unteren paar Bits des Codes. Einige Sammlungen basieren auf der Annahme, dass Hashes über einen größeren Raum verteilt sind, sodass die Verteilung der Bits dazu beitragen kann, dass die Sammlung effizienter arbeitet.
public void Add(string value, StrinComparison comparison);
Nichts: Der Parameter StringComparison
sollte comparisonType
heißen, um der Benennung zu entsprechen, die überall sonst verwendet wird, wo StringComparison
als Parameter verwendet wird.
Die Kriterien, die uns bei der Auswahl von Algorithmen helfen würden, wären:
Was wir wirklich gerne sehen würden, sind Leistungszahlen für Kandidaten, die in C# geschrieben wurden, damit wir einigermaßen sicher sein können, dass ihre Eigenschaften für .NET Bestand haben. Wenn Sie einen Kandidaten schreiben und wir ihn dafür nicht auswählen, wird das immer noch nützliche Arbeit sein, wenn ich tatsächlich den API-Vorschlag für die nicht-kryptografische Hash-API zusammenstelle.
Hier sind einige Kandidaten, von denen ich denke, dass sie es wert sind, bewertet zu werden (aber zögern Sie nicht, andere vorzuschlagen):
Schade, dass die Add
Methoden nicht den Rückgabetyp ref HashCode
und ref this
damit sie flüssig verwendet werden können.
Würden readonly ref
Rückgaben dies zulassen? /cc @jaredpar @VSadov
WARNUNG: Wenn jemand eine Hash-Implementierung aus einer bestehenden Codebasis irgendwo im Internet auswählt, behalten Sie bitte den Link zur Quelle und überprüfen Sie die Lizenz (wir müssen dies auch tun).
Wenn die Lizenz nicht kompatibel ist, müssen wir den Algorithmus möglicherweise von Grund auf neu schreiben.
IMO sollte die Verwendung der Add-Methoden äußerst selten sein. Es wird für sehr fortgeschrittene Szenarien sein, und die Notwendigkeit, "fließend" zu sein, wird nicht wirklich vorhanden sein.
Für die üblichen Anwendungsfälle für 99% aller Benutzercode-Fälle sollte man in der Lage sein, einfach => HashCode.Combine(...)
und gut zu sein.
@morganbr
wir könnten auch
public static int Combine<T1>(T1 value);
. Ich weiß, es sieht ein bisschen komisch aus, aber es würde eine Möglichkeit bieten, Bits von etwas mit einem begrenzten Eingabe-Hash-Raum zu diffundieren
Sinn ergeben. Ich habe es hinzugefügt.
@justinvp
Nichts: Der Parameter
StringComparison
solltecomparisonType
heißen, um der Benennung zu entsprechen, die überall sonst verwendet wird, woStringComparison
als Parameter verwendet wird.
Fest.
@CyrusNajmabadi
IMO, die Verwendung der
Add
Methoden sollte äußerst ungewöhnlich sein. Es wird für sehr fortgeschrittene Szenarien sein, und die Notwendigkeit, "fließend" zu sein, wird nicht wirklich vorhanden sein.
Einverstanden.
@benaadams - re: ref gibt this
von Add
- nein, this
kann nicht von ref in struct-Methoden zurückgegeben werden, da es ein rValue oder ein temp sein kann.
```C#
ref var r = (neues T()).ReturnsRefThis();
// r bezieht sich hier auf eine Variable. Welcher? Was ist der Umfang/die Lebensdauer?
r = Etwas Anderes ();
```
Im Fall ist es für Vergleichszwecke nützlich, vor einigen Jahren ich die portierte Jenkins lookup3 Hash - Funktion ( C - hier .
Ich wundere mich über Sammlungen:
@terrajobst
c# public void Add<T>(T[] value);
Warum gibt es eine Überladung für Arrays, aber keine für allgemeine Sammlungen (zB IEnumerable<T>
)?
Ist es nicht verwirrend, dass sich HashCode.Combine(array)
und hashCode.Add((object)array)
eine Richtung verhalten (Verwendung von Referenzgleichheit) und hashCode.Add(array)
sich
@CyrusNajmabadi
Für die üblichen Anwendungsfälle für 99% aller Benutzercode-Fälle sollte man in der Lage sein, einfach
=> HashCode.Combine(...)
und gut zu sein.
Wenn es wirklich darum geht, Combine
in 99 % der Anwendungsfälle (und nicht etwa in 80 %) verwenden zu können, dann sollte Combine
irgendwie Hashing-Sammlungen basierend auf den Werten unterstützen in der Sammlung? Vielleicht sollte es eine separate Methode geben, die dies tut (entweder eine Erweiterungsmethode oder eine statische Methode auf HashCode
)?
Wenn Add ein Power-Szenario ist, sollten wir davon ausgehen, dass der Benutzer zwischen Object.GetHashCode und dem Kombinieren einzelner Elemente von Sammlungen wählen sollte? Wenn es helfen würde, könnten wir erwägen, das Array (und potenzielle IEnumerable) Versionen umzubenennen. Etwas wie:
c#
public void AddEnumerableHashes<T>(IEnumerable<T> enumerable);
public void AddEnumerableHashes<T>(T[] array);
public void AddEnumerableHashes<T>(T[] array, int index, int length);
Ich frage mich, ob wir mit IEqualityComparers auch Überladungen brauchen würden.
Vorschlag: Lassen Sie die Builder-Struktur IEnumerable
implementieren, um die Syntax des Sammlungsinitialisierers zu unterstützen:
C#
return new HashCode {
SomeField,
OtherField,
{ SomeString, StringComparer.UTF8 },
{ SomeHashSet, HashSet<int>.CreateSetComparer() }
}.GetHashCode()
Das ist viel eleganter, als Add()
von Hand aufzurufen (insbesondere braucht man keine temporäre Variable) und hat immer noch keine Zuweisungen.
@SLaks Vielleicht könnte diese schönere Syntax auf https://github.com/dotnet/csharplang/issues/455 warten (vorausgesetzt, dass der Vorschlag unterstützt wird), damit HashCode
nicht gefälschte IEnumerable
implementieren müsste
Wir haben uns entschieden, GetHashCode() nicht zu überschreiben, um den Hash-Code zu erzeugen, da dies sowohl in Bezug auf die Benennung als auch aus verhaltenstechnischer Sicht seltsam wäre (GetHashCode() sollte den Hash-Code des Objekts zurückgeben, nicht den berechneten).
Ich finde es seltsam, dass GetHashCode
den berechneten Hashcode nicht zurückgibt. Ich denke, das wird die Entwickler verwirren. Zum Beispiel hat @SLaks es bereits in seinem Vorschlag verwendet, anstatt ToHashCode
.
@justinvp Wenn GetHashCode()
den berechneten Hash-Code nicht zurückgibt , sollte er wahrscheinlich mit [Obsolete]
und [EditorBrowsable(Never)]
.
Auf der anderen Seite sehe ich keinen Schaden darin, den berechneten Hash-Code zurückzugeben.
@terrajobst
Wir haben uns entschieden,
GetHashCode()
nicht zu überschreiben, um den Hash-Code zu erzeugen, da dies sowohl von der Benennung als auch vom Standpunkt des Verhaltens aus seltsam wäre (GetHashCode()
sollte den Hash-Code des Objekts zurückgeben, nicht den einen berechnet).
Ja, GetHashCode()
sollte den Hashcode des Objekts zurückgeben, aber gibt es einen Grund, warum die beiden Hashcodes unterschiedlich sein sollten? Es ist immer noch richtig, da zwei Instanzen von HashCode
mit demselben internen Status denselben Wert von GetHashCode()
.
@terrajobst Ich habe gerade deinen Kommentar gesehen. Verzeihen Sie mir die verspätete Antwort, ich habe mir die Benachrichtigung nur langsam angesehen, weil ich dachte, es würde nur mehr hin und her gehen, das nirgendwo hinführt. Schön zu sehen, dass dem nicht so ist! :tada:
Ich würde es gerne aufnehmen und die Durchsatz-/Verteilungsmessung durchführen (ich nehme an, das meinten Sie mit "interessiert an der Arbeit in diesem Bereich"). Geben Sie mir jedoch eine Sekunde, um alle Kommentare hier zu Ende zu lesen.
@terrajobst
Können wir uns ändern
public void Add<T>(T[] value);
public void Add<T>(T[] value, int index, int length);
public void Add(byte[] value);
public void Add(byte[] value, int index, int length);
zu
public void AddRange<T>(T[] values);
public void AddRange<T>(T[] values, int index, int count);
public void AddRange<T>(T[] values, int index, int count, IEqualityComparer<T> comparer);
? Ich habe Add
-> AddRange
, um das von @svick erwähnte Verhalten zu vermeiden. Ich habe die byte
Überladungen entfernt, da wir uns mit typeof(T) == typeof(byte)
innerhalb der Methode spezialisieren können, wenn wir etwas bytespezifisches tun müssen. Außerdem habe ich value
-> values
und length
-> count
geändert. Es ist auch sinnvoll, eine Vergleicherüberladung zu haben.
@terrajobst Kannst du mich daran erinnern, warum
public void Add(string value);
public void Add(string value, StringComparison comparisonType);
ist notwendig, wenn wir haben
public void Add<T>(T value);
public void Add<T>(T value, IEqualityComparer<T> comparer);
?
@svick
@justinvp Wenn GetHashCode() den berechneten Hash-Code nicht zurückgibt, sollte er wahrscheinlich als [Obsolete] und [EditorBrowsable(Never)] markiert werden.
:+1:
@terrajobst Können wir zu einer impliziten Konvertierung von edit: HashCode
-> int
, also keine ToHashCode
Methode?ToHashCode
ist in Ordnung. Siehe die Antwort von @CyrusNajmabadi unten.
@jamesqo StringComparison
ist eine Aufzählung.
Die Leute könnten jedoch stattdessen das Äquivalent von StringComparer
verwenden.
Können wir zu einer impliziten Konvertierung von HashCode -> int zurückkehren, also keine ToHashCode-Methode?
Wir haben dies diskutiert und uns in der Sitzung dagegen entschieden. Das Problem ist, dass, wenn der Benutzer das letzte "int" erhält, oft zusätzliche Arbeit geleistet wird. dh die Interna des Hashcodes führen oft einen Finalisierungsschritt durch und können sich selbst in einen neuen Zustand zurücksetzen. Das mit einer impliziten Konvertierung passieren zu lassen, wäre seltsam. Wenn Sie dies getan haben:
HashCode hc = ...
int i1 = hc;
int i2 = hc;
Dann kann es zu unterschiedlichen Ergebnissen kommen.
Aus diesem Grund mögen wir auch die explizite Konvertierung nicht (da die Leute Konvertierungen nicht als Änderung des internen Zustands betrachten).
Mit einer Methode können wir explizit dokumentieren, dass dies geschieht. Wir können es möglicherweise sogar benennen, um es so zu vermitteln. zB "ToHashCodeAndReset" (obwohl wir uns dagegen entschieden haben). Aber zumindest kann die Methode eine klare Dokumentation enthalten, die der Benutzer in Dingen wie Intellisense sehen kann. Bei Konvertierungen ist das nicht der Fall.
Ich habe die Byte-Überladungen entfernt, da wir uns mit typeof(T) == typeof(byte) spezialisieren können
IIRC gab es einige Bedenken, dass dies aus der JIT-Perspektive nicht in Ordnung ist. Aber das war möglicherweise nur für die Nicht-Wert-Typ-"typeof()"-Fälle der Fall. Solange der Jit für die Fälle vom Werttyp typeof() effektiv das Richtige tut, sollte das gut sein.
@CyrusNajmabadi Ich war mir nicht bewusst, dass die Konvertierung in ein int
einen mutierenden Zustand beinhalten könnte. ToHashCode
es dann.
Für diejenigen, die über die Krypto-Perspektive nachdenken - http://tuprints.ulb.tu-darmstadt.de/2094/1/thesis.lehmann.pdf
@terrajobst , hatten Sie Zeit, meine Kommentare (ab hier ) zu lesen und zu entscheiden, ob Sie mit der optimierten API-Form einverstanden sind? Wenn ja, dann denke ich, dass dies als API-genehmigt / zum Greifen nah markiert werden kann und wir uns für einen Hash-Algorithmus entscheiden können.
@blowdart ,
Ich habe es oben vielleicht nicht zu explizit gemacht, aber die einzigen nicht-kryptografischen Hashes, die ich von HashDoS-Einbrüchen nicht kenne, sind Marvin und SipHash. Das heißt, selbst das Seeding (sagen wir) Murmur mit einem zufälligen Wert kann immer noch gebrochen und für ein DoS verwendet werden.
Keine, ich fand es nur interessant, und ich denke, die Dokumentation dazu sollte sagen "Nicht für Hash-Codes geeignet, die über kryptografische Algorithmen generiert werden."
Entscheidungen
AddRange
Methoden entfernen, da das Szenario unklar ist. Es ist eher unwahrscheinlich, dass Arrays sehr oft auftauchen. Und wenn es sich um größere Arrays handelt, stellt sich die Frage, ob die Berechnung zwischengespeichert werden soll. Wenn Sie die for-Schleife auf der aufrufenden Seite sehen, wird deutlich, dass Sie darüber nachdenken müssen.IEnumerable
Überladungen zu AddRange
hinzufügen, weil sie allokiert würden.Add
brauchen, die string
und StringComparison
. Ja, diese sind wahrscheinlich effizienter als Anrufe über IEqualityComparer
, aber wir können dies später beheben.GetHashCode
mit einem Fehler als veraltet zu markieren, aber wir gehen noch einen Schritt weiter und verstecken uns auch vor IntelliSense.Dies lässt uns mit:
```C#
// Wird in der Kernbaugruppe leben
// .NET Framework: mscorlib
// .NET Core: System.Runtime / System.Private.CoreLib
Namensraum-System
{
öffentliche Struktur HashCode
{
public static int Kombinieren
public static int Kombinieren
public static int Kombinieren
public static int Kombinieren
public static int Kombinieren
public static int Kombinieren
public static int Kombinieren
public static int Kombinieren
public void Add<T>(T value);
public void Add<T>(T value, IEqualityComparer<T> comparer);
[Obsolete("Use ToHashCode to retrieve the computed hash code.", error: true)]
[EditorBrowsable(Never)]
public override int GetHashCode();
public int ToHashCode();
}
}
```
Nächste Schritte: Das Problem ist greifbar – um die API zu implementieren, die wir mit mehreren Kandidatenalgorithmen als Experimente https://github.com/dotnet/corefx/issues/14354#issuecomment -305028686 für eine Liste, damit wir entscheiden können, welcher Algorithmus verwendet werden soll (basierend auf Durchsatz- und Verteilungsmessungen, wahrscheinlich unterschiedliche Antwort pro CPU-Architektur).
Komplexität: Groß
Wenn jemand daran interessiert ist, es abzuholen, ping uns bitte. Es könnte sogar Platz für mehrere Personen sein, die gemeinsam daran arbeiten. ( @jamesqo Sie haben die Priorität, da Sie am meisten und am längsten in die Ausgabe investiert haben)
@karelz Trotz meines obigen Kommentars habe ich meine Meinung geändert, weil ich glaube nicht, dass ich die Qualifikationen habe, den besten Hash-Algorithmus auszuwählen. Ich habe mir einige der aufgelisteten Bibliotheken @morganbr angesehen und festgestellt, dass die Implementierung ziemlich komplex ist , sodass ich sie nicht einfach in C# übersetzen kann, um sie selbst zu testen. Ich habe wenig Hintergrundwissen in C++, daher würde es mir auch schwer fallen, nur die Bibliothek zu installieren und eine Test-App zu schreiben.
Ich möchte jedoch nicht, dass dies für immer auf der Liste bleibt. Wenn sich ab heute in einer Woche niemand mehr damit beschäftigt, werde ich in Betracht ziehen, eine Frage auf Programmers SE oder Reddit zu stellen.
Ich habe es nicht auf den Prüfstand gestellt (oder anderweitig optimiert), aber hier ist eine grundlegende Implementierung des Murmur3-Hash-Algorithmus, den ich in mehreren meiner persönlichen Projekte verwende: https://gist.github.com/tannergooding/0a12559d1a912068b9aeb4b9586aad7f
Ich denke, die optimale Lösung hier besteht darin, den Hashing-Algorithmus basierend auf der Größe der Eingabedaten dynamisch zu ändern.
Beispiel: Mumur3 (und andere) sind sehr schnell für große Datenmengen und bieten eine große Verteilung, aber sie können bei kleineren Datenmengen "schlecht" (Geschwindigkeit, nicht Verteilung) funktionieren.
Ich stelle mir vor, wir sollten etwas tun wie: Wenn die Gesamtbyteanzahl kleiner als X ist, führen Sie Algorithmus A aus; andernfalls führen Sie Algorithmus B aus. Dieser ist immer noch deterministisch (pro Durchlauf), ermöglicht es uns jedoch, Geschwindigkeit und Verteilung basierend auf der tatsächlichen Größe der Eingabedaten bereitzustellen.
Es ist wahrscheinlich auch erwähnenswert, dass einige der genannten Algorithmen Implementierungen haben, die speziell für SIMD-Befehle entwickelt wurden, so dass eine der leistungsfähigsten Lösungen wahrscheinlich eine FCALL auf einer bestimmten Ebene beinhalten würde (wie bei einigen der BufferCopy-Implementierungen) oder eine Abhängigkeit beinhalten kann auf System.Numerics.Vector
.
@jamesqo , wir helfen gerne bei der Auswahl; Was wir am meisten brauchen, sind Leistungsdaten für Kandidatenimplementierungen (idealerweise C#, obwohl @tannergooding betont , dass einige Algorithmen spezielle Compilerunterstützung benötigen). Wie ich oben erwähnt habe, werden wir, wenn Sie einen Kandidaten erstellen, der nicht ausgewählt wurde, wahrscheinlich später verwenden, also machen Sie sich keine Sorgen, dass Arbeit verschwendet wird.
Ich weiß, dass es Benchmarks für verschiedene Implementierungen gibt, aber ich denke, es ist wichtig, einen Vergleich mit dieser API und einem wahrscheinlichen Bereich von Eingaben (zB Strukturen mit 1-10 Feldern) zu haben.
@tannergooding , diese Art von Anpassungsfähigkeit ist möglicherweise am leistungsfähigsten, aber ich sehe nicht, wie sie mit der Add-Methode funktionieren würde, da sie nicht weiß, wie oft sie aufgerufen wird. Obwohl wir dies mit Combine tun könnten, würde dies bedeuten, dass eine Reihe von Add-Aufrufen zu einem anderen Ergebnis führen könnte als der entsprechende Combine-Aufruf.
Da der wahrscheinlichste Eingabebereich 4-32 Bytes beträgt ( Combine`1
- Combine`8
), gibt es in diesem Bereich hoffentlich keine großen Leistungsänderungen.
diese Art von Anpassungsfähigkeit ist möglicherweise am leistungsfähigsten, aber ich sehe nicht, wie sie mit der Add-Methode funktionieren würde, da sie nicht weiß, wie oft sie aufgerufen wird.
Ich persönlich bin nicht davon überzeugt, dass die API-Form für allgemeines Hashing ganz richtig ist (es ist jedoch naheliegend) ...
Derzeit stellen wir Combine
Methoden für die statische Konstruktion zur Verfügung. Wenn diese dazu gedacht sind, alle Eingaben zu kombinieren und einen endgültigen Hash-Code zu erzeugen, dann ist der Name 'schlecht' und etwas wie Compute
könnte passender sein.
Wenn wir Combine
Methoden verfügbar machen, sollten sie nur alle Eingaben mischen und die Benutzer sollten aufgefordert werden, eine Finalize
Methode aufzurufen, die die Ausgabe der letzten Kombination sowie die Gesamtzahl der Bytes, die waren kombiniert, um einen finalisierten Hash-Code zu erzeugen (das Finalisieren eines Hash-Codes ist wichtig, da er die Lawinen der Bits verursacht).
Für das Builder-Muster stellen wir eine Methode Add
und ToHashCode
bereit. Es ist nicht klar, ob die Methode Add
dazu gedacht ist, die Bytes zu speichern und nur beim Aufruf von ToHashCode
kombinieren/abzuschließen (in diesem Fall können wir den richtigen Algorithmus dynamisch auswählen) oder ob dies der Fall ist die spontan kombiniert werden sollen, sollte klar sein, dass dies der Fall ist (und dass die Implementierung die Gesamtgröße der kombinierten Bytes intern verfolgen sollte).
Für alle, die einen weniger komplizierten Ausgangspunkt suchen, versuchen Sie es mit xxHash32. Das lässt sich wahrscheinlich ziemlich leicht in C# übersetzen (die Leute haben es getan ).
Ich teste immer noch lokal, aber ich sehe die folgenden Durchsatzraten für meine C#-Implementierung von Murmur3.
Dies sind die statischen Combine-Methoden für 1-8 Eingaben:
1070.18 mb/s
1511.49 mb/s
1674.89 mb/s
1957.65 mb/s
2083.24 mb/s
2140.94 mb/s
2190.27 mb/s
2245.53 mb/s
Meine Implementierung geht davon aus, dass GetHashCode
für jede Eingabe aufgerufen werden sollte und dass der berechnete Wert vor der Rückgabe abgeschlossen werden sollte.
Ich habe int
Werte kombiniert, da sie am einfachsten zu testen sind.
Um den Durchsatz zu berechnen, habe ich 10.001 Iterationen ausgeführt und die erste Iteration als "Aufwärmlauf" verworfen.
In jeder Iteration führe ich 10.000 Unteriterationen aus, wobei ich HashCode.Combine
aufrufe und das Ergebnis der vorherigen Unteriteration als ersten Eingabewert in der nächsten Iteration übergebe.
Ich durchschnittliche dann alle Iterationen, um die durchschnittliche verstrichene Zeit zu erhalten, und dividiere diese weiter durch die Anzahl der Unteriterationen, die pro Schleife ausgeführt werden, um die durchschnittliche Zeit pro Aufruf zu erhalten. Ich berechne dann die Anzahl der Anrufe, die pro Sekunde getätigt werden können, und multipliziere diese mit der Anzahl der kombinierten Bytes, um den tatsächlichen Durchsatz zu berechnen.
Werde den Code bereinigen und in Kürze teilen.
@tannergooding , das klingt nach einem großen Fortschritt. Um sicherzustellen, dass Sie die richtigen Messwerte erhalten, ist die Absicht der API, dass ein Aufruf von HashCode.Combine(a, b)
einem Aufruf entspricht
HashCode hc = new HashCode();
hc.Add(a); // Initializes the hash state, calls a.GetHashCode() and feeds the result into the hash state
hc.Add(b); // Calls b.GetHashCode() and feeds the result into the hash state
return hc.ToHashCode(); // Finalizes the hash state, truncates it to an int, resets the internal state and returns the int
In beiden Fällen sollten die Daten in den gleichen internen Hash-Zustand eingespeist und der Hash am Ende einmal finalisiert werden.
👍
Das ist effektiv, was der Code, den ich geschrieben habe, tut. Der einzige Unterschied besteht darin, dass ich den gesamten Code effektiv einfüge (es besteht keine Notwendigkeit, new HashCode()
zuzuweisen und die Anzahl der kombinierten Bytes zu verfolgen, da sie konstant ist).
@morganbr. Implementierung + Durchsatztest für Murmur3: https://gist.github.com/tannergooding/89bd72f05ab772bfe5ad3a03d6493650
MurmurHash3 basiert auf dem hier beschriebenen Algorithmus: https://github.com/aappleby/smhasher/wiki/MurmurHash3 , Repo sagt, es sei MIT
Arbeiten an xxHash32 (BSD-2-Klausel -- https://github.com/Cyan4973/xxHash/blob/dev/xxhash.c) und SpookyHash (Public Domain -- http://www.burtleburtle.net/bob/hash /spooky.html) Varianten
@tannergooding Nochmal, kein Hash-Experte, aber ich erinnerte mich, [einen Artikel zu lesen [1], der sagte, Murmur sei nicht DoS-resistent, also habe ich nur darauf hingewiesen, bevor wir uns dafür entscheiden.
@jamesqo , ich könnte mich irren, aber ich bin mir ziemlich sicher, dass die Sicherheitsanfälligkeit auf Murmur2 und nicht auf Murmur3 zutrifft.
In beiden Fällen implementiere ich mehrere Algorithmen, damit wir Durchsatzergebnisse für C# erhalten. Die Verteilung und andere Eigenschaften dieser Algorithmen sind ziemlich bekannt, sodass wir später den besten auswählen können 😄
Hoppla, vergessen, den Artikel zu verlinken: http://emboss.github.io/blog/2012/12/14/breaking-murmur-hash-flooding-dos-reloaded/.
@tannergooding OK. Klingt fair :+1:
@tannergooding , ich habe mir deine Murmur3-Implementierung angesehen und sie sieht im Allgemeinen richtig und wahrscheinlich ziemlich gut optimiert aus. Um sicherzustellen, dass ich es richtig verstehe, verwenden Sie die Tatsache, dass CombinedValue und der interne Zustand von Murmur beide 32 Bit haben? Das ist wahrscheinlich eine ziemlich gute Optimierung für diesen Fall und erklärt einige meiner früheren Verwirrung.
Wenn wir es übernehmen würden, braucht es möglicherweise ein paar Optimierungen (sie werden jedoch wahrscheinlich keinen großen Unterschied bei den Perf-Messungen machen):
In der Zwischenzeit, während ich mich nach dieser API sehne, wie schlimm ist es für mich, GetHashCode über (field1, field2, field3).GetHashCode()
zu implementieren?
@jnm2 , der ValueTuple-Hash-Code-Combiner neigt dazu, Ihre Eingaben im Hash-Code zu ordnen (und die am wenigsten aktuellen zu verwerfen). Bei einigen Feldern und einer Hash-Tabelle, die durch eine Primzahl dividiert wird, bemerken Sie es möglicherweise nicht. Bei vielen Feldern oder einer Hash-Tabelle, die durch eine Zweierpotenz dividiert wird, hat die Entropie des zuletzt eingefügten Felds den größten Einfluss darauf, ob Kollisionen auftreten (z 'wird wahrscheinlich viele Kollisionen haben, wenn es ein Guid ist, werden Sie wahrscheinlich nicht).
ValueTuple funktioniert auch nicht gut mit Feldern, die alle 0 sind.
Nebenbei bemerkt musste ich die Arbeit an anderen Implementierungen einstellen (mit höherer Priorität arbeiten). Ich bin mir nicht sicher, wann ich es wieder abholen kann.
Wenn das für einen strukturierten Typ nicht gut genug ist, warum ist es dann gut genug für ein Tupel?
@jnm2 , das ist einer der Gründe, warum es sich lohnt, diese Funktion zu entwickeln - damit wir minderwertige Hashes im gesamten Framework ersetzen können.
Große Tabelle von Hashfunktionen mit Leistungs- und Qualitätsmerkmalen:
https://github.com/leo-yuriev/t1ha
@arespr Ich denke, das Team sucht nach einer C#
@tannergooding Können Sie dieses Problem immer noch nicht
edit: Habe einen Beitrag auf Reddit gemacht. https://www.reddit.com/r/csharp/comments/6qsysm/Looking_for_hash_expert_to_help_net_core_team/?ref=share&ref_source=link
@jamesqo , ich habe ein paar Dinge mit höherer Priorität auf meinem Teller und werde in den nächsten 3 Wochen nicht dazu kommen.
Außerdem werden die aktuellen Messungen durch das begrenzt, was wir derzeit in C# codieren können. Wenn dies jedoch zur Sache wird (https://github.com/dotnet/designs/issues/13), werden sich die Messungen wahrscheinlich etwas ändern ;)
Außerdem werden die aktuellen Messungen durch das begrenzt, was wir derzeit in C# codieren können. Wenn dies jedoch zu einer Sache wird (dotnet/designs#13), werden sich die Messungen wahrscheinlich etwas ändern ;)
Das ist in Ordnung – wir können den Hash-Algorithmus jederzeit ändern, sobald intrinsische Elemente verfügbar sind. Das Einkapseln/Randomisieren des Hash-Codes ermöglicht uns dies. Wir suchen nur nach etwas, das in seinem aktuellen Zustand den besten Kompromiss zwischen Leistung und Verteilung für die Laufzeit bietet.
@jamesqo , danke, dass hast , die
Hi! Ich habe mir gerade die Diskussion durchgelesen, und zumindest scheint mir der Fall zugunsten des murmur3-32 PoC entschieden geschlossen zu sein. Das scheint mir übrigens eine sehr gute Wahl zu sein, und ich würde empfehlen, keine unnötige Arbeit mehr aufzuwenden (aber vielleicht sogar die .Add()
Mitglieder fallen zu lassen ...).
Aber für den unwahrscheinlichen Fall, dass jemand mit mehr Performance-Arbeiten fortfahren möchte, könnte ich Code für xx32, xx64, hsip13/24, seahash, murmur3-x86/32 (und ich habe das marvin32-Impl von oben integriert) und (noch nicht optimiert) sip13/24, spookyv2. Einige Versionen von City lassen sich bei Bedarf einfach portieren. Dieses halb aufgegebene Projekt hatte einen etwas anderen Anwendungsfall im Sinn, daher gibt es keine HashCode-Klasse mit der vorgeschlagenen API; aber für Benchmarking sollte es nicht viel ausmachen.
Definitiv nicht produktionsreif: Der Code verwendet großzügige Mengen an Brute-Force wie Copy-Pasta, krebsartige Ausbreitung von Aggressiv-Inline und unsicher; Endianness existiert nicht, ebensowenig unausgerichtete Reads. Selbst Tests gegen ref-impl Testvektoren sind beschönigend "unvollständig".
Wenn das überhaupt hilft, sollte ich in den nächsten zwei Wochen genug Zeit finden, um die ungeheuerlichsten Probleme zu beheben und den Code und einige vorläufige Ergebnisse zur Verfügung zu stellen.
@gimpf
Ich habe mir gerade die Diskussion durchgelesen, und zumindest scheint mir der Fall zugunsten des murmur3-32 PoC entschieden geschlossen zu sein. Was mir übrigens eine sehr gute Wahl ist, und ich würde empfehlen, keine unnötige Arbeit mehr aufzuwenden
Nein, die Leute bevorzugen Murmur3 noch nicht. Wir möchten sicherstellen, dass wir den absolut besten Algorithmus in Bezug auf das Gleichgewicht zwischen Leistung und Verteilung auswählen, damit wir nichts unversucht lassen können.
Aber für den unwahrscheinlichen Fall, dass jemand mit mehr Performance-Arbeiten fortfahren möchte, könnte ich Code für xx32, xx64, hsip13/24, seahash, murmur3-x86/32 (und ich habe das marvin32-Impl von oben integriert) und (noch nicht optimiert) sip13/24, spookyv2. Einige Versionen von City lassen sich bei Bedarf einfach portieren.
Ja bitte! Wir wollen Code für so viele Algorithmen wie möglich sammeln, gegen die wir testen können. Jeder neue Algorithmus, den Sie beitragen können, ist wertvoll. Es wäre sehr wünschenswert, wenn Sie auch die City-Algorithmen portieren könnten.
Definitiv nicht produktionsreif: Der Code verwendet großzügige Mengen an Brute-Force wie Copy-Pasta, krebsartige Ausbreitung von Aggressiv-Inline und unsicher; Endianness existiert nicht, ebensowenig unausgerichtete Reads. Selbst Tests gegen ref-impl Testvektoren sind beschönigend "unvollständig".
Das ist ok. Bringen Sie einfach den Code ein, und jemand anderes kann ihn bei Bedarf finden.
Wenn das überhaupt hilft, sollte ich in den nächsten zwei Wochen genug Zeit finden, um die ungeheuerlichsten Probleme zu beheben und den Code und einige vorläufige Ergebnisse zur Verfügung zu stellen.
Ja das wäre toll!
@jamesqo Ok, ich werde eine Notiz
@gimpf das hört sich wirklich toll an und wir würden gerne von deinen Fortschritten hören (kein Warten, bis du jeden Algorithmus
Ich habe keine Analyse gesehen, wie die Entropie von Seahash im Vergleich zu anderen Algorithmen abschneidet. Haben Sie dazu Hinweise? Es hat interessant klingende Kompromisse bei der Leistungsfähigkeit ... Vektorisierung klingt schnell, aber modulare Arithmetik klingt langsam.
@morganbr Ich habe einen Teaser parat.
Über SeaHash : Nein, ich weiß noch nichts über die Qualität; Falls die Leistung interessant ist, würde ich sie zu SMasher hinzufügen. Zumindest behauptet der Autor, dass es gut ist (verwendet es für Prüfsummen in einem Dateisystem) und behauptet auch, dass beim Mischen keine Entropie weggeworfen wird.
Zu den Hashes und Benchmarks : Projekt Haschisch.Kastriert , Wiki-Seite mit ersten Benchmarking-Ergebnissen im Vergleich von xx32, xx64, hsip13, hsip24, marvin32, sea und murmur3-32.
Einige wichtige Vorbehalte:
Erste Eindrücke:
HashSet<>
muss bearbeitet werden, da alles fast innerhalb des Messfehlers liegt (ich habe größere Unterschiede gesehen, aber immer noch nicht der Rede wert)Ich schreibe dir nochmal, sobald ich die Situation etwas verbessert habe.
@gimpf , das ist ein fantastischer Start! Ich habe mir den Code und die Ergebnisse angesehen und habe ein paar Fragen.
Ihre HashSet-Ergebnisse sind besonders interessant. Wenn sie sich halten, ist dies ein möglicher Fall dafür, eine bessere Entropie einer schnelleren Hash-Zeit vorzuziehen.
@morganbr Dieses Wochenende war eher
Zu Ihren Fragen:
- Ihre Ergebnisse zeigen, dass SimpleMultiplyAdd etwa 5x langsamer ist als Murmur3a von @tannergooding. Das scheint seltsam...
Ich habe mich selbst gewundert. Das war ein Kopier-/Einfügefehler, SimpleMultiplyAdd kombinierte immer vier Werte... Außerdem wurde der Multiply-Add-Combiner durch die Neuordnung einiger Anweisungen etwas schneller (~60% höherer Durchsatz).
Ist es möglich, dass Ihre Implementierungen eine gemeinsame Ineffizienz aufweisen, die nicht in dieser Murmur-Implementierung vorhanden ist, oder sollte ich dies als benutzerdefinierte Implementierungen lesen, die einen großen Vorteil gegenüber Allzweckimplementierungen haben?
Ich vermisse wahrscheinlich einige Dinge, aber es scheint, dass für .NET allgemeine Implementierungen für diesen Anwendungsfall nicht verwendbar sind. Ich habe Methoden im Combine-Stil für alle Algorithmen geschrieben, und die meisten Algorithmen mit Hash-Code-Kombinationen sind _viel_ besser als die Allzweck-Methoden.
Allerdings bleiben selbst diese Implementierungen zu langsam; weitere Arbeiten sind erforderlich. .NET-Performance in diesem Bereich ist für mich absolut undurchsichtig; Das Hinzufügen oder Entfernen einer Kopie einer lokalen Variablen kann die Leistung leicht um den Faktor zwei ändern. Ich werde wahrscheinlich nicht in der Lage sein, Implementierungen bereitzustellen, die ausreichend gut optimiert sind, um die beste Option auszuwählen.
- Es ist gut, Ergebnisse für 1, 2 und 4 Kombinationen zu haben, aber diese API geht bis zu 8.
Ich habe die Mähdrescher-Benchmarks erweitert. Keine Überraschungen an dieser Front.
- Ich habe gesehen, dass Sie auf X64 laufen (...). Ist es für Sie einfach, auch X86-Ergebnisse zu erhalten?
Es war einmal, aber dann habe ich auf .NET Standard portiert. Jetzt bin ich in der Abhängigkeits-Hölle und nur .NET Core 2 und CLR 64-Bit-Benchmarks funktionieren. Dies kann leicht genug gelöst werden, sobald ich die aktuellen Probleme gelöst habe.
Glaubst du, das wird es in der Version 2.1 schaffen?
@gimpf Sie haben eine Weile nichts gepostet - haben Sie ein Fortschrittsupdate zu Ihren Implementierungen? :smiley:
@jamesqo Ich habe einige Benchmarks behoben, die zu seltsamen Ergebnissen geführt haben, und City32, SpookyV2, Sip13 und Sip24 zur Liste der verfügbaren Algorithmen hinzugefügt. Die Sips sind erwartungsgemäß schnell (relativ zum Durchsatz von xx64), City und Spooky sind es nicht (das gleiche gilt immer noch für SeaHash).
Für die Kombination von Hash-Codes sieht Murmur3-32 immer noch wie eine gute Wahl aus, aber ich muss noch einen umfassenderen Vergleich durchführen.
Außerdem hat die Streaming-API (.Add()) den unglücklichen Nebeneffekt, dass einige Hash-Algorithmen von der Kandidatenliste entfernt werden. Da auch die Leistungsfähigkeit einer solchen API fraglich ist, sollten Sie überdenken, ob Sie sie von Anfang an anbieten.
Wenn der .Add()
Teil vermieden würde und der Hash-Combiner einen Seed verwendet, würde es meiner Meinung nach nicht schaden, den Combiner von tg zu bereinigen und eine kleine Testsuite zu erstellen, und nennen es einen Tag. Da ich jedes Wochenende nur wenige Stunden zur Verfügung habe und die Leistungsoptimierung etwas mühsam ist, könnte sich das Anfertigen der vergoldeten Version etwas in die Länge ziehen...
@gimpf , das klingt nach einem tollen Fortschritt. Haben Sie eine Ergebnistabelle zur Hand, damit wir sehen können, ob es genug gibt, um eine Entscheidung zu treffen und voranzukommen?
@morganbr Ich habe meine Benchmarking-Ergebnisse aktualisiert.
Im Moment habe ich nur 64-Bit-Ergebnisse für .NET Core 2. Für diese Plattform ist City64 ohne Seed über alle Größen hinweg am schnellsten. XX-32 enthält einen Samen und ist mit Murmur-3-32 verbunden. Glücklicherweise sind dies dieselben Algorithmen, die für 32-Bit-Plattformen den Ruf haben, schnell zu sein, aber wir müssen natürlich überprüfen, dass dies auch für meine Implementierung gilt. Die Ergebnisse scheinen für die reale Leistung repräsentativ zu sein, außer dass Sea und SpookyV2 ungewöhnlich langsam erscheinen.
Sie müssen sich überlegen, wie viel Hash-Dos-Schutz für Hash-Code-Kombinierer wirklich benötigt wird. Wenn das Seeding nur erforderlich ist, um den Hash für die Persistenz offensichtlich unbrauchbar zu machen, wäre eine XOR-Verknüpfung von city64 mit einem 32-Bit-Seed eine Verbesserung. Da dieses Dienstprogramm nur dazu da ist, Hashes zu kombinieren (und nicht beispielsweise den Hash-Code für Strings ersetzen oder ein Drop-In-Hasher für Integer-Arrays usw. sein), könnte das gut genug sein.
Wenn Sie OTOH denken, dass Sie es brauchen, werden Sie froh sein zu sehen, dass Sip13 normalerweise weniger als 50% langsamer ist als XX-32 (auf 64-Bit-Plattformen), aber dieses Ergebnis wird wahrscheinlich für 32-Bit-Apps erheblich anders sein.
Ich weiß nicht, wie sehr es für Corefx relevant ist, aber ich habe LegacyJit 32-Bit-Ergebnisse (mit FW 4.7) hinzugefügt.
Ich möchte sagen, dass die Ergebnisse lächerlich langsam sind. Aber als Beispiel, bei 56 MiB/s vs. 319 MiB/s lache ich nicht (das ist Sip, es fehlt am meisten die Rotations-Links-Optimierung). Ich glaube, ich erinnere mich, warum ich mein .NET-Hash-Algorithmus-Projekt im Januar abgebrochen habe...
RyuJit-32bit fehlt also immer noch und wird (hoffentlich) ganz andere Ergebnisse liefern, aber für LegacyJit-x86 gewinnt Murmur-3-32 praktisch, und nur City-32 und xx-32 können nahe kommen. Murmur hat mit nur etwa 0,4 bis 1,1 GB/s statt 0,6 bis 2 GB/s (auf dem gleichen Rechner) immer noch eine schlechte Leistung, aber immerhin liegt es im richtigen Rahmen.
Ich werde heute Abend die Benchmarks auf einigen meiner Boxen durchführen und Ergebnisse veröffentlichen (Ryzen, i7, Xeon, A10, i7 Mobile und ich denke, ein paar andere).
@tannergooding @morganbr Einige nette und einige wichtige Updates.
Wichtig zuerst:
Schöne Dinge:
So führen Sie eine Suite auf allen Prime-Implementierungen zum Kombinieren von Hash-Codes aus, einschließlich "Empty" (reiner Overhead) und "Multiply-Add" (geschwindigkeitsoptimierte Version der berühmten SO-Antwort):
bin\Release\net47\Haschisch.Benchmarks.Net47.exe -j:clr_x86 -j:clr_x64_legacy -j:clr_x64 -j:core_x64 -- CombineHashCode --allcategories=prime
(_Das Ausführen von 32-Bit-Core-Benchmarks scheint praktischerweise eine Vorabversion von BenchmarkDotNet zu erfordern (oder vielleicht ein Nur-32-Bit-Setup plus den Core-basierten Bench-Runner). Es sollte dann mit -j:core_x86 funktionieren, hoffentlich)_
Ergebnisse : Nach allen Fehlerbehebungen scheint xx32 bei allen Überlastungen mit 64-Bit-RyuJIT unter Windows 10 auf einem mobilen Haswell i7 in einem "schnellen" Durchlauf zu gewinnen. Zwischen den Sips und marvin32 gewinnt immer Sip-1-3. Sip-1-3 ist etwa 4-mal langsamer als xx32, was wiederum etwa 2-mal langsamer ist als ein primitiver Multiply-Add-Combiner. 32-Bit-Core-Ergebnisse fehlen noch, aber ich warte mehr oder weniger auf eine stabile BenchmarkDotNet-Version, die dieses Problem für mich löst.
(Bearbeiten) Ich habe gerade einen Schnelldurchlauf eines Benchmarks für den Zugriff auf ein Hash-Set hinzugefügt. Dies hängt offensichtlich viel mehr von Details ab als die obigen µ-Benchmarks, aber Sie sollten es sich vielleicht ansehen.
Nochmals vielen Dank
Zunächst würde ich die Algorithmen wie folgt aufteilen:
Fast+Good Entropie (geordnet nach Geschwindigkeit):
HashDoS-resistent:
Außer Konkurrenz (langsam):
Außer Konkurrenz (schlechte Entropie):
Bevor wir einen Gewinner auswählen, möchte ich sicherstellen, dass andere Leute mit meiner obigen Einteilung einverstanden sind. Wenn es hält, denke ich, müssen wir nur entscheiden, ob wir 2x für HashDoS-Resistenz bezahlen und dann nach Geschwindigkeit gehen.
@morganbr Ihre Gruppierung scheint in Ordnung zu sein. Als Datenpunkt in SipHash-Runden fragte das Rust-Projekt Jean-Philippe Aumasson , der sip-hash w/DJB verfasst hat. Nach dieser Diskussion entschieden sie sich für sip-1-3 für Hash-Tabellen.
(Siehe PR rust:#33940 und die begleitende Ausgabe rust:#29754 ).
Basierend auf den Daten und Kommentaren möchte ich vorschlagen, dass wir xxHash32 auf allen Architekturen verwenden. Der nächste Schritt ist die Implementierung. @gimpf , hast du erstellen ?
Für diejenigen, die sich über HashDoS Sorgen machen, werde ich demnächst einen Vorschlag für eine Allzweck-Hashing-API machen, die Marvin32 enthalten sollte und SipHash enthalten kann. Das wird auch ein geeigneter Ort für die anderen Implementierungen sein, an denen @gimpf und @tannergooding gearbeitet haben.
@morganbr Ich kann eine PR zusammenstellen, wenn es die Zeit erlaubt. Außerdem würde ich persönlich auch xx32 bevorzugen, solange es die Akzeptanz nicht verringert.
@gimpf , wie sieht deine Zeit aus? Wenn Sie nicht wirklich Zeit haben, können wir auch sehen, ob jemand anderes es ausprobieren möchte.
@morganbr Ich hatte geplant, es bis zum 5. November zu machen, und es sieht immer noch gut aus, dass ich die Zeit in den nächsten zwei Wochen finde.
@gimpf , hört sich
@terrajobst - Ich bin etwas spät
```c#
öffentlicher HashCode hinzufügen
öffentlicher HashCode hinzufügen
The params code is clearly there for scenarios where you have multiple fields, e.g.
```c#
public override int GetHashCode() => new HashCode().Add(Name, Surname).ToHashCode();
Genau dasselbe kann jedoch wie folgt erreicht werden, wenn auch mit einer weniger verschwenderischen Array-Zuweisung:
c#
public override int GetHashCode() => new HashCode().Add(Name).Add(Surname).Add(Age).ToHashCode();
Beachten Sie, dass Typen auch gemischt werden können. Dies könnte offensichtlich dadurch erreicht werden, dass es innerhalb einer regulären Methode nicht fließend aufgerufen wird. Angesichts dieses Arguments, dass die fließende Schnittstelle nicht unbedingt erforderlich ist, warum gibt es dann die verschwenderische params
Überladung überhaupt? Wenn dieser Vorschlag ein schlechter Vorschlag ist, fällt die Überladung von params
auf dieselbe Axt. Das und das Erzwingen einer regulären Methode für einen trivialen, aber optimalen Hashcode scheint eine Menge Zeremonie zu sein.
Edit: Ein implicit operator int
wäre auch schön für DRY, aber nicht unbedingt entscheidend.
@jcdickinson
können wir den Rückgabetyp der Add-Methode nicht ändern?
Das haben wir bereits im alten Vorschlag besprochen, und es wurde abgelehnt.
warum gibt es die verschwenderische params-Überladung überhaupt?
Wir fügen keine Params-Überladungen hinzu? Führen Sie auf dieser Webseite Strg+F für "params" aus, und Sie werden sehen, dass Ihr Kommentar die einzige Stelle ist, an der dieses Wort auftaucht.
Ein impliziter Operator int wäre auch schön für DRY, aber nicht unbedingt entscheidend.
Ich glaube das wurde oben auch schon besprochen...
@jamesqo danke für die Erklärung.
Parameterüberladungen
Ich meinte AddRange
, aber ich denke, da wird es keine Anziehungskraft geben.
@jcdickinson AddRange
war im ursprünglichen Vorschlag enthalten, aber nicht in der aktuellen Version. Es wurde von der API-Überprüfung abgelehnt (siehe https://github.com/dotnet/corefx/issues/14354#issuecomment-308190321 von @terrajobst):
Wir sollten alle
AddRange
Methoden entfernen, da das Szenario unklar ist. Es ist eher unwahrscheinlich, dass Arrays sehr oft angezeigt werden. Und wenn es sich um größere Arrays handelt, stellt sich die Frage, ob die Berechnung zwischengespeichert werden soll. Wenn Sie die for-Schleife auf der aufrufenden Seite sehen, wird deutlich, dass Sie darüber nachdenken müssen.
@gimpf Ich habe den Vorschlag mit xxHash32 mehrfach gefüllt . Fühlen Sie sich frei, diese Implementierung zu greifen. Es hat Tests gegen tatsächliche xxHash32-Vektoren.
Apropos Schnittstelle. Ich bin mir vollkommen bewusst, dass ich aus einem Maulwurfshügel einen Berg mache - ignoriere es einfach. Ich verwende den aktuellen Vorschlag gegen echtes Zeug und es gibt viele nervige Wiederholungen.
Ich habe mit der Benutzeroberfläche herumgespielt und verstehe jetzt, warum die flüssige Benutzeroberfläche abgelehnt wurde; es ist deutlich langsamer.
BenchmarkDotNet=v0.10.9, OS=Windows 10 Redstone 2 (10.0.15063)
Processor=Intel Core i7-4800MQ CPU 2.70GHz (Haswell), ProcessorCount=8
Frequency=2630626 Hz, Resolution=380.1377 ns, Timer=TSC
.NET Core SDK=2.0.2
[Host] : .NET Core 2.0.0 (Framework 4.6.00001.0), 64bit RyuJIT
DefaultJob : .NET Core 2.0.0 (Framework 4.6.00001.0), 64bit RyuJIT
Verwenden einer nicht eingebetteten Methode als Hashcodequelle; 50 Aufrufe von Add im Vergleich zu einer fließenden Erweiterungsmethode:
| Methode | Mittelwert | Fehler | StdDev | Skaliert |
|------- |---------:|---------:|---------:|-------: |
| Hinzufügen | 401,6 ns | 1,262 ns | 1,180 ns | 1,00 |
| Zählung | 747,8 ns | 2.329 ns | 2,178 ns | 1,86 |
Das folgende Muster funktioniert jedoch:
```c#
öffentliche Struktur HashCode : System.Collections.IEnumerable
{
[EditorBrowsable(EditorBrowsableState.Never)]
[Obsolete("Diese Methode wird für die Syntax des Sammlungsinitialisierers bereitgestellt.", error: true)]
public IEnumerator GetEnumerator() => throw new NotImplementedException();
}
public override int GetHashCode() => new HashCode()
{
Age, // int
{ Name, StringComparer.Ordinal }, // use Comparer
Hat // some arbitrary object
}.ToHashCode();
```
Es hat auch identische Leistungsmerkmale wie der aktuelle Vorschlag:
| Methode | Mittelwert | Fehler | StdDev | Skaliert |
|------------ |--------:|--------:|---------:|----- ----:|
| Hinzufügen | 405,0 ns | 2.130 ns | 1,889 ns | 1,00 |
| Initialisierer | 400,8 ns | 4,821 ns | 4.274 ns | 0,99 |
Leider ist es ein bisschen ein Hack, da IEnumerable
implementiert werden muss, um den Compiler glücklich zu machen. Davon abgesehen wird das Obsolete
sogar bei foreach
Obsolete
einen Fehler foreach
- Sie müssten wirklich Dinge unterbrechen wollen, um auf die Ausnahme zu stoßen. Die MSIL ist bei beiden im Wesentlichen identisch.
@jcdickinson danke für das greifen des Problems. Ich habe Ihnen eine Mitarbeiter-Einladung gesendet, lassen Sie es mich wissen, wenn Sie annehmen, und ich kann Ihnen dieses Problem zuweisen (in der Zwischenzeit mir selbst zuordnen).
Profi-Tipp: Sobald Sie zustimmen, wird GitHub Sie automatisch für alle Benachrichtigungen aus dem Repo anmelden (500+ pro Tag). du abonniert hast.
@jcdickinson , ich bin auf jeden Fall daran interessiert, lästige Wiederholungen zu vermeiden (obwohl ich keine Ahnung habe, wie die Leute über die Initialisierungssyntax denken würden). Ich scheine mich zu erinnern, dass es zwei Probleme mit dem Fließen gab:
var hc = new HashCode();
var newHc = hc.Add(foo);
hc.Add(bar);
return newHc.ToHashCode();
Da der Vorschlag für diesen Thread bereits genehmigt wurde (und Sie auf dem besten Weg sind, ihn zusammenzuführen), würde ich vorschlagen, für alle Änderungen einen neuen API-Vorschlag zu erstellen.
@karelz Ich glaube, @gimpf hat sich dieses Thema schon vorher bitte stattdessen edit: nvm)
@terrajobst Eine Art Last-Minute-API-Anfrage dafür. Da wir GetHashCode
veraltet markiert haben, teilen wir dem Benutzer implizit mit, dass HashCode
s keine Werte sind, die verglichen werden sollen, obwohl es sich um Strukturen handelt, die normalerweise unveränderlich/vergleichbar sind. Sollten wir in diesem Fall auch Equals
veraltet markieren?
[Obsolete("HashCode is a mutable struct and should not be compared with other HashCodes.", error: true)]
[EditorBrowsable(Never)]
// If this is too harsh, base.Equals() is fine as long as the [Obsolete] stays
public override bool Equals(object obj) => throw new NotSupportedException("HashCode is a mutable struct and should not be compared with other HashCodes.");
Ich denke, etwas Ähnliches wurde mit Span
.
Wenn das akzeptiert wird, dann denke ich...
should not
oder may not
anstelle von cannot
in der Obsolete-Nachricht zu verwenden.@ Joe4evr Gut mit mir; Ich habe den Kommentar aktualisiert. Es kann auch von Vorteil sein, dieselbe Nachricht auch in die Ausnahme GetHashCode
, dann:
public override int GetHashCode() => throw new NotSupportedException("HashCode is a mutable struct and should not be compared with other HashCodes.");
@morganbr Warum hast du das wieder geöffnet?
Die PR, um es in CoreFX zu veröffentlichen, ist noch nicht abgeschlossen.
@gimpf Haben Sie den Code, den Sie Benchmarking durchgeführt haben, zur Verfügung und/oder könnten Sie schnell sehen, wie das SpookilySharp-Nuget-Paket abschneidet. Ich möchte dieses Projekt nach ein paar Jahren Stagnation abstauben und bin gespannt, wie es sich behauptet.
@JonHanna Er hat es hier gepostet: https://github.com/gimpf/Haschisch.Kastriert
@JonHanna , es würde mich interessieren, wie Ihre Tests verlaufen, damit wir darüber nachdenken können, was in einer universellen nicht-kryptografischen Hashing-API nützlich wäre.
@morganbr Wo wäre ein geeignetes Forum, um eine solche API zu diskutieren? Ich gehe davon aus, dass eine solche API aus mehr als nur dem kleinsten gemeinsamen Nenner bestehen würde, und vielleicht braucht eine gute API auch eine verbesserte JIT bezüglich der Handhabung größerer Strukturen. Was besser gemacht werden könnte, besprechen wir in einer separaten Ausgabe...
@gimpf Hat eine für dich
@morganbr - Können wir den Paketnamen und die Versionsnummer
@karelz , könnten Sie @smitpatel mit Paket-/Versionsinformationen helfen?
Ich würde den täglichen Build von .NET Core ausprobieren - ich würde bis morgen warten.
Ich glaube nicht, dass es ein Paket gibt, von dem man einfach abhängig sein kann.
Frage an die Teilnehmer hier. Die Roslyn-IDE ermöglicht es Benutzern, eine GetHashCode-Impl basierend auf einer Reihe von Feldern/Eigenschaften in ihrer Klasse/Struktur zu generieren. Im Idealfall könnten die Leute das neue HashCode.Combine verwenden, das in https://github.com/dotnet/corefx/pull/25013 hinzugefügt wurde. Einige Benutzer haben jedoch keinen Zugriff auf diesen Code. Wir möchten also immer noch in der Lage sein, einen GetHashCode zu generieren, der für sie funktioniert.
Kürzlich ist uns aufgefallen, dass das von uns generierte Formular problematisch ist. Nämlich, weil VB standardmäßig mit aktivierten Überlaufprüfungen kompiliert und unser Impl Überläufe verursacht. Außerdem hat VB keine Möglichkeit, Überlaufprüfungen für eine Coderegion zu deaktivieren. Sie ist für die gesamte Baugruppe entweder ein- oder ausgeschaltet.
Aus diesem Grund würde ich gerne das von uns bereitgestellte Impl durch ein Formular ersetzen, das nicht unter diesen Problemen leidet. Idealerweise hätte das generierte Formular die folgenden Eigenschaften:
a + b + c + d
oder a ^ b ^ c ^ d
bekommt.Eine Option für VB wäre beispielsweise, Folgendes zu generieren:
return (a, b, c, d).GetHashCode()
Dies hängt jedoch davon ab, ob ein Verweis auf System.ValueTuple vorhanden ist. Im Idealfall könnten wir ein Impl haben, das auch ohne dieses funktioniert.
Kennt jemand einen anständigen Hashing-Algorithmus, der mit diesen Einschränkungen arbeiten kann? Vielen Dank!
--
Hinweis: Unser vorhandener ausgegebener Code ist:
Dim hashCode = -252780983
hashCode = hashCode * -1521134295 + i.GetHashCode()
hashCode = hashCode * -1521134295 + j.GetHashCode()
Return hashCode
Dies kann eindeutig überlaufen.
Dies ist auch für C# kein Problem, da wir einfach unchecked { }
um diesen Code herum hinzufügen können. Diese feinkörnige Steuerung ist in VB nicht möglich.
Kennt jemand einen anständigen Hashing-Algorithmus, der mit diesen Einschränkungen arbeiten kann? Vielen Dank!
Nun, Sie könnten Tuple.Create(...).GetHashCode()
tun. Offensichtlich zieht dies Zuweisungen nach sich, aber es scheint besser zu sein, als eine Ausnahme auszulösen.
Gibt es einen Grund, warum Sie dem Benutzer nicht einfach sagen können, dass er System.ValueTuple
installieren soll? Da es sich um eine eingebaute Sprachfunktion handelt, bin ich mir sicher, dass das System.ValueTuple-Paket mit praktisch allen Plattformen sehr kompatibel ist, oder?
Offensichtlich zieht dies Zuweisungen nach sich, aber es scheint besser zu sein, als eine Ausnahme auszulösen.
Jawohl. Es wäre schön, wenn es keine Zuordnungen verursachen würde.
Gibt es einen Grund, warum Sie dem Benutzer nicht einfach sagen können, dass er System.ValueTuple installieren soll?
Das wäre das Verhalten, wenn wir den ValueTuple-Ansatz generieren. Aber auch hier wäre es schön, wenn wir einfach etwas Gutes generieren könnten, das zu der Art und Weise passt, wie der Benutzer seinen Code derzeit strukturiert hat, ohne ihn dazu zu bringen, seine Struktur schwergewichtig zu ändern.
Es scheint wirklich so, als ob VB-Benutzer eine Möglichkeit haben sollten, dieses Problem auf vernünftige Weise anzugehen :) Aber ein solcher Ansatz entzieht sich mir :)
@CyrusNajmabadi , Wenn Sie wirklich Ihre eigene Hash-Berechnung im Code des Benutzers durchführen müssen, könnte CRC32 funktionieren, da es eine Kombination aus Tabellensuchen und XORs ist (aber keine Arithmetik, die überlaufen kann). Es gibt jedoch einige Nachteile:
Wenn Sie es noch nicht tun, hoffe ich, dass Sie den HashCode-Typ erkennen und ihn nach Möglichkeit verwenden können, da XXHash viel besser sein sollte.
@morganbr Siehe https://github.com/dotnet/roslyn/pull/24161
Wir machen folgendes:
Return (a, b, c, ...).GetHashCode()
Es ist '3d', das ist wirklich schade. Grundsätzlich kann jemand, der VB verwendet, aber kein ValueTuple oder ein aktuelles System verwendet, uns nicht verwenden, um einen vernünftigen Hash-Algorithmus für ihn zu generieren.
Sie müssten irgendwo im Code eine Lookup-Tabelle mit 256 Einträgen einfügen
Das wäre völlig ungenießbar :)
Ist Code zur Tabellengenerierung auch ungenießbar? Zumindest nach Wikipedia-Beispiel ist es nicht viel Code (aber es muss immer noch irgendwo in der Quelle des Benutzers stehen).
Wie schrecklich wäre es, die HashCode-Quelle zum Projekt hinzuzufügen, wie es Roslyn (mit IL) mit (den viel einfacheren) Compiler-Attributklassendefinitionen tut, wenn sie nicht über eine referenzierte Assembly verfügbar sind?
Wie schrecklich wäre es, die HashCode-Quelle dem Projekt hinzuzufügen, wie es Roslyn mit (den viel einfacheren) Compiler-Attributklassendefinitionen tut, wenn sie nicht über eine referenzierte Assembly verfügbar sind?
Ich bin nur überrascht, dass es keine guten Möglichkeiten gibt, die Überlaufmathematik in VB zum Laufen zu bringen :(
Selbst wenn wir also zwei Werte miteinander hashen würden, scheint es, als müssten wir zumindest Folgendes erstellen:
```c#
var hc1 = (uint) (Wert1?.GetHashCode() ?? 0); // kann überlaufen
var hc2 = (uint)(value2?.GetHashCode() ?? 0); // kann überlaufen
uint hash = MixEmptyState();
hash += 8; // can overflow
hash = QueueRound(hash, hc1);
hash = QueueRound(hash, hc2);
hash = MixFinal(hash);
return (int)hash; // can overflow
Note that this code already has 4 lines that can overflow. It also has two helper functions you need to call (i'm ignoring MixEmptyState as that seems more like a constant). MixFinal can *definitely* overflow:
```c#
private static uint MixFinal(uint hash)
{
hash ^= hash >> 15;
hash *= Prime2;
hash ^= hash >> 13;
hash *= Prime3;
hash ^= hash >> 16;
return hash;
}
ebenso wie QueueRound:
c#
private static uint QueueRound(uint hash, uint queuedValue)
{
hash += queuedValue * Prime3;
return Rol(hash, 17) * Prime4;
}
Also ich sehe ehrlich gesagt nicht, wie das funktionieren soll :(
Wie schrecklich wäre es, die HashCode-Quelle dem Projekt hinzuzufügen, wie es Roslyn (mit IL) mit (den viel
Wie stellen Sie sich das Funktionieren vor? Was würden die Kunden schreiben und was würden die Compiler dann tun?
Außerdem würde dies alles angehen, wenn .Net bereits öffentliche Helfer auf der Oberflächen-API enthält, die ohne Überlauf von uint in int32 (und umgekehrt) konvertieren.
Existieren die? Wenn ja, kann ich leicht die VB-Versionen schreiben und diese einfach für die Situationen verwenden, in denen wir zwischen den Typen wechseln müssen, ohne überzulaufen.
Ist Code zur Tabellengenerierung auch ungenießbar?
Ich würde so denken. Ich meine, denk mal aus der Kundenperspektive. Sie wollen nur eine anständige GetHashCode-Methode, die schön in sich geschlossen ist und vernünftige Ergebnisse liefert. Es wird ziemlich unangenehm sein, diese Funktion zu verwenden und ihren Code mit Hilfsmüll aufzublähen. Es ist auch ziemlich schlecht, wenn man bedenkt, dass die C#-Erfahrung in Ordnung sein wird.
Sie können möglicherweise ungefähr das richtige Überlaufverhalten erzielen, indem Sie in und aus einer Kombination von 64-Bit-Typen mit und ohne Vorzeichen umwandeln. Etwa so (ungetestet und ich kenne die VB-Casting-Syntax nicht):
Dim hashCode = -252780983
hashCode = (Int32)((Int32)((Unt64)hashCode * -1521134295) + (UInt64)i.GetHashCode())
Woher wissen Sie, dass Folgendes nicht überläuft?
c#
(Int32)((Unt64)hashCode * -1521134295)
Oder die endgültige (int32) Besetzung für diese Angelegenheit?
Ich wusste nicht, dass es überlaufgeprüfte Conv-Operationen verwenden würde. Ich denke, Sie könnten es vor dem Casting auf 32 Bit maskieren:
(Int32)(((Unt64)hashCode * -1521134295) & 0xFFFFFFFF)
vermutlich 31 Bit, da ein Wert von uint32.Max auch bei der Konvertierung in Int32 überlaufen würde :)
Das ist auf jeden Fall möglich. Hässlich... aber möglich :) Es gibt viele Besetzungen in diesem Code.
Okay. Ich denke, ich habe eine praktikable Lösung. Der Kern des Algorithmus, den wir heute generieren, ist:
c#
hashCode = hashCode * -1521134295 + j.GetHashCode();
Nehmen wir an, wir machen 64-Bit-Mathematik, aber "hashCode" wurde auf 32 Bit begrenzt. Dann wird <largest_32_bit> * -1521134295 + <largest_32_bit>
64 Bit nicht überlaufen. Wir können also immer in 64 Bit rechnen und dann auf 32 (oder 32 Bit) reduzieren, um sicherzustellen, dass die nächste Runde nicht überläuft.
Vielen Dank!
@MaStr11 @morganbr @sharwell und alle hier. Ich habe meinen Code aktualisiert, um Folgendes für VB zu generieren:
Dim hashCode As Long = 2118541809
hashCode = (hashCode * -1521134295 + a.GetHashCode()) And Integer.MaxValue
hashCode = (hashCode * -1521134295 + b.GetHashCode()) And Integer.MaxValue
Return CType(hashCode And Integer.MaxValue, Integer)
Kann mich jemand vernünftig überprüfen, um sicherzustellen, dass dies Sinn macht und auch bei aktiviertem Modus nicht überlaufen sollte?
@ CyrusNajmabadi , das wird nicht überlaufen (weil Int64.Max = Int32.Max * Int32.Max und Ihre Konstanten viel kleiner sind), aber Sie maskieren das hohe Bit auf Null, es ist also nur ein 31-Bit-Hash. Wird es als Überlauf betrachtet, das High-Bit eingeschaltet zu lassen?
@CyrusNajmabadi hashCode
ist ein Long
, das zwischen 0 und Integer.MaxValue
. Warum bekomme ich das?
Aber nein, es kann nicht wirklich überlaufen.
Übrigens: Ich würde Roslyn lieber ein NuGet-Paket hinzufügen lassen, als einen suboptimalen Hash hinzuzufügen.
aber Sie maskieren das High-Bit auf Null, also ist es nur ein 31-Bit-Hash. Wird es als Überlauf betrachtet, das High-Bit eingeschaltet zu lassen?
Das ist ein guter Punkt. Ich glaube, ich dachte an einen anderen Algorithmus, der uints verwendet. Um also sicher von der langen in eine uint zu konvertieren, musste ich das Vorzeichenbit nicht einschließen. Da dies jedoch alles Mathematik mit Vorzeichen ist, denke ich, dass es in Ordnung wäre, nur gegen 0xffffffff zu maskieren, um sicherzustellen, dass wir nach dem Hinzufügen jedes Eintrags nur die unteren 32 Bit beibehalten.
Ich würde Roslyn lieber ein NuGet-Paket hinzufügen lassen, als einen suboptimalen Hash hinzuzufügen.
Benutzer können dies bereits tun, wenn sie möchten. Hier geht es darum, was zu tun ist, wenn Benutzer diese Abhängigkeiten nicht hinzufügen können oder können. Dabei geht es auch darum, den Benutzern einen einigermaßen „gut genug“ Hash bereitzustellen. dh etwas Besseres als der übliche "x + y + z"-Ansatz, den die Leute oft verfolgen. Es ist nicht als „optimal“ gedacht, da es keine gute Definition dafür gibt, was „optimal“ in Bezug auf Hashing für alle Benutzer ist. Beachten Sie, dass wir hier den Ansatz verfolgen, der bereits vom Compiler für anonyme Typen ausgegeben wird. Es zeigt ein einigermaßen gutes Verhalten, während es dem Code des Benutzers nicht viel Komplexität hinzufügt. Im Laufe der Zeit, da immer mehr Benutzer in der Lage sind, sich vorwärts zu bewegen, können solche langsam verschwinden und für die meisten Menschen durch HashCode.Combine ersetzt werden.
Also habe ich ein bisschen daran gearbeitet und mir folgendes einfallen lassen, das meiner Meinung nach alle Bedenken adressiert:
Dim hashCode As Long = 2118541809
hashCode = (hashCode * -1521134295 + a.GetHashCode()).GetHashCode()
hashCode = (hashCode * -1521134295 + b.GetHashCode()).GetHashCode()
Return CType(hashCode, Integer)
Der interessanteste Teil ist der Aufruf von .GetHashCode()
für den int64-Wert, der von (hashCode * -1521134295 + a.GetHashCode())
. Der Aufruf von .GetHashCode für diesen 64-Bit-Wert hat zwei gute Eigenschaften für unsere Bedürfnisse. Erstens stellt es sicher, dass hashCode immer nur einen zulässigen int32-Wert darin speichert (was die endgültige Rückgabe immer sicher macht). Zweitens stellt es sicher, dass wir keine wertvollen Informationen in den oberen 32 Bit des int64-Temp-Werts verlieren, mit dem wir arbeiten.
@CyrusNajmabadi Eigentlich wollte ich das Paket installieren, wonach ich gefragt habe. Erspart mir das machen zu müssen.
Wenn Sie HashCode eingeben und System.HashCode in einem MS-Nuget-Paket bereitgestellt wird, bietet Roslyn es an.
Ich möchte, dass es die nicht vorhandene GetHashCode-Überladung generiert und das Paket im selben Vorgang installiert.
Ich denke, das ist für die meisten Benutzer keine geeignete Wahl. Das Hinzufügen von Abhängigkeiten ist ein sehr schwerer Vorgang, zu dem Benutzer nicht gezwungen werden sollten. Benutzer können den richtigen Zeitpunkt für diese Entscheidungen festlegen, und die IDE wird dies respektieren. Das war der Ansatz, den wir bisher bei all unseren Funktionen verfolgt haben, und es ist ein gesunder Ansatz, den die Leute zu mögen scheinen.
Hinweis: In welchem Nuget-Paket ist diese API überhaupt enthalten, damit wir einen Verweis hinzufügen können?
Die Implementierung befindet sich in System.Private.CoreLib.dll, würde also als Teil des Laufzeitpakets enthalten sein. Der Vertrag ist System.Runtime.dll.
Okay. Wenn dies der Fall ist, hört es sich so an, als würde ein Benutzer dies erhalten, wenn/wenn er zu einem neueren Target-Framework wechselt. So etwas ist überhaupt kein Schritt, den ich mit dem "Generieren von Equals + Hashcode" für das Projekt eines Benutzers ausführen lassen würde.
Hilfreichster Kommentar
Entscheidungen
AddRange
Methoden entfernen, da das Szenario unklar ist. Es ist eher unwahrscheinlich, dass Arrays sehr oft auftauchen. Und wenn es sich um größere Arrays handelt, stellt sich die Frage, ob die Berechnung zwischengespeichert werden soll. Wenn Sie die for-Schleife auf der aufrufenden Seite sehen, wird deutlich, dass Sie darüber nachdenken müssen.IEnumerable
Überladungen zuAddRange
hinzufügen, weil sie allokiert würden.Add
brauchen, diestring
undStringComparison
. Ja, diese sind wahrscheinlich effizienter als Anrufe überIEqualityComparer
, aber wir können dies später beheben.GetHashCode
mit einem Fehler als veraltet zu markieren, aber wir gehen noch einen Schritt weiter und verstecken uns auch vor IntelliSense.Dies lässt uns mit:
```C#(T1-Wert1);(T1-Wert1, T2-Wert2);(T1-Wert1, T2-Wert2, T3-Wert3);(T1-Wert1, T2-Wert2, T3-Wert3, T4-Wert4);(T1-Wert1, T2-Wert2, T3-Wert3, T4-Wert4, T5-Wert5);(T1-Wert1, T2-Wert2, T3-Wert3, T4-Wert4, T5-Wert5, T6-Wert6);(T1-Wert1, T2-Wert2, T3-Wert3, T4-Wert4, T5-Wert5, T6-Wert6, T7-Wert7);(T1-Wert1, T2-Wert2, T3-Wert3, T4-Wert4, T5-Wert5, T6-Wert6, T7-Wert7, T8-Wert8);
// Wird in der Kernbaugruppe leben
// .NET Framework: mscorlib
// .NET Core: System.Runtime / System.Private.CoreLib
Namensraum-System
{
öffentliche Struktur HashCode
{
public static int Kombinieren
public static int Kombinieren
public static int Kombinieren
public static int Kombinieren
public static int Kombinieren
public static int Kombinieren
public static int Kombinieren
public static int Kombinieren
}
```