Runtime: Proposition : ajoutez System.HashCode pour faciliter la génération de bons codes de hachage.

Créé le 9 déc. 2016  ·  182Commentaires  ·  Source: dotnet/runtime

Mise à jour 16/06/17 : Recherche de bénévoles

La forme API a été finalisée. Cependant, nous sommes toujours en train de décider du meilleur algorithme de hachage parmi une liste de candidats à utiliser pour la mise en œuvre, et nous avons besoin de quelqu'un pour nous aider à mesurer le débit/la distribution de chaque algorithme. Si vous souhaitez assumer ce rôle, veuillez laisser un commentaire ci-dessous et @karelz vous attribuera ce problème.

Mise à jour 13/06/17 : Proposition acceptée !

Voici l'API qui a été approuvée par @terrajobst à l' adresse https://github.com/dotnet/corefx/issues/14354#issuecomment -308190321 :

// 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();
    }
}

Le texte original de cette proposition suit.

Raisonnement

La génération d'un bon code de hachage ne devrait pas nécessiter l'utilisation de constantes magiques laides et de peu de manipulation sur notre code. Il devrait être moins tentant d'écrire une implémentation GetHashCode mauvaise mais concise telle que

class Person
{
    public override int GetHashCode() => FirstName.GetHashCode() + LastName.GetHashCode();
}

Proposition

Nous devrions ajouter un type HashCode pour encapsuler la création de code de hachage et éviter de forcer les développeurs à se mêler aux détails désordonnés. Voici ma proposition, qui est basée sur https://github.com/dotnet/corefx/issues/14354#issuecomment -305019329, avec quelques révisions mineures.

// 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();
    }
}

Remarques

Voir @terrajobst commentaire de » à https://github.com/dotnet/corefx/issues/14354#issuecomment -305019329 pour les objectifs de cette API; toutes ses remarques sont valables. Je voudrais cependant souligner ceux-ci en particulier :

  • L'API n'a pas besoin de produire un hachage cryptographique fort
  • L'API fournira "un" code de hachage, mais ne garantira pas un algorithme de code de hachage particulier. Cela nous permet d'utiliser un algorithme différent plus tard ou d'utiliser des algorithmes différents sur des architectures différentes.
  • L'API garantira que dans un processus donné, les mêmes valeurs produiront le même code de hachage. Différentes instances de la même application produiront probablement des codes de hachage différents en raison de la randomisation. Cela nous permet de garantir que les consommateurs ne peuvent pas conserver les valeurs de hachage et s'appuyer accidentellement sur leur stabilité à travers les exécutions (ou pire, les versions de la plate-forme).
api-approved area-System.Numerics up-for-grabs

Commentaire le plus utile

Les décisions

  • Nous devrions supprimer toutes les méthodes AddRange car le scénario n'est pas clair. Il est peu probable que les tableaux apparaissent très souvent. Et une fois que de plus grands tableaux sont impliqués, la question est de savoir si le calcul doit être mis en cache. Voir la boucle for du côté appelant montre clairement que vous devez y penser.
  • Nous ne voulons pas non plus ajouter IEnumerable surcharges AddRange car elles seraient allouées.
  • Nous ne pensons pas avoir besoin de la surcharge de Add qui prend string et StringComparison . Oui, ceux-ci sont probablement plus efficaces que d'appeler via le IEqualityComparer , mais nous pouvons résoudre ce problème plus tard.
  • Nous pensons que marquer GetHashCode comme obsolète avec erreur est une bonne idée, mais nous irions un peu plus loin et nous nous cacherions également d'IntelliSense.

Cela nous laisse avec :

```C#
// vivra dans l'assemblage de base
// .NET Framework : mscorlib
// .NET Core : System.Runtime / System.Private.CoreLib
système d'espace de noms
{
structure publique HashCode
{
public statique int Combiner(valeur T11) ;
public statique int Combiner(valeur T11, valeur T22) ;
public statique int Combiner(valeur T11, valeur T22, valeur T33) ;
public statique int Combiner(valeur T11, valeur T22, valeur T33, valeur T44) ;
public statique int Combiner(valeur T11, valeur T22, valeur T33, valeur T44, valeur T55) ;
public statique int Combiner(valeur T11, valeur T22, valeur T33, valeur T44, valeur T55, valeur T66) ;
public statique int Combiner(valeur T11, valeur T22, valeur T33, valeur T44, valeur T55, valeur T66, valeur T77) ;
public statique int Combiner(valeur T1, valeur T22, valeur T33, valeur T44, valeur T55, valeur T66, valeur T77, valeur T88) ;

    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();
}

}
```

Tous les 182 commentaires

Proposition : ajouter la prise en charge de la randomisation par hachage

public static HashCode Randomized<T> { get; } // or CreateRandomized<T>
or 
public static HashCode Randomized(Type type); // or CreateRandomized(Type type)

T ou Type type est nécessaire pour obtenir le même hachage aléatoire pour le même type.

Proposition : ajouter un support pour les collections

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);

Je pense qu'il n'y a pas besoin de surcharges Combine(_field1, _field2, _field3, _field4, _field5) car le prochain code HashCode.Empty.Combine(_field1).Combine(_field2).Combine(_field3).Combine(_field4).Combine(_field5); devrait être optimisé en ligne sans appels Combine.

@AlexRadch

Proposition : ajouter un support pour les collections

Oui, cela faisait partie de mon plan éventuel pour cette proposition. Je pense qu'il est important de se concentrer sur la façon dont nous voulons que l'API ressemble avant d'ajouter ces méthodes, cependant.

Il voulait utiliser un algorithme différent, comme le hachage Marvin32 qui est utilisé pour les chaînes dans coreclr. Cela nécessiterait d'étendre la taille de HashCode à 8 octets.

Qu'en est-il des types Hash32 et Hash64 qui stockeraient en interne 4 ou 8 octets de données ? Documentez les avantages/inconvénients de chacun. Hash64 est bon pour X, mais potentiellement plus lent. Hash32 étant plus rapide, mais potentiellement pas aussi distribué (ou quel que soit le compromis).

Il voulait randomiser la graine de hachage, afin que les hachages ne soient pas déterministes.

Cela semble être un comportement utile. Mais je pouvais voir des gens vouloir contrôler ça. Alors peut-être qu'il devrait y avoir deux façons de créer le Hash, une qui ne prend aucune graine (et utilise une graine aléatoire) et une qui permet à la graine d'être fournie.

Remarque : Roslyn aimerait si cela pouvait être fourni dans le fichier Fx. Nous ajoutons une fonctionnalité pour cracher un GetHashCode pour l'utilisateur. Actuellement, il génère du code comme :

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; }

Ce n'est pas une grande expérience, et cela expose de nombreux concepts laids. Nous serions ravis d'avoir une API Hash.Whatever que nous pourrions appeler à la place.

Merci!

Et MurmurHash ? Il est raisonnablement rapide et possède de très bonnes propriétés de hachage. Il existe également deux implémentations différentes, une qui crache des hachages 32 bits et une autre qui crache des hachages 128 bits.

Il existe également des implémentations vectorisées pour les formats 32 bits et 128 bits.

@tannergooding MurmurHash est rapide, mais pas sécurisé, à partir des sons de cet article de blog .

@jkotas , y a-t-il eu des travaux dans le JIT pour générer un meilleur code pour les structures > 4 octets sur 32 bits depuis nos discussions l'année dernière ? Aussi, que pensez-vous de la proposition de

Qu'en est-il des types Hash32 et Hash64 qui stockeraient en interne 4 ou 8 octets de données ? Documentez les avantages/inconvénients de chacun. Hash64 est bon pour X, mais potentiellement plus lent. Hash32 étant plus rapide, mais potentiellement pas aussi distribué (ou quel que soit le compromis).

Je pense toujours que ce type serait très précieux à proposer aux développeurs et ce serait formidable de l'avoir en 2.0.

@jamesqo , je ne pense pas que cette implémentation

De plus, cet article s'applique à Murmur2. Le problème a été résolu dans l'algorithme Murmur3.

le JIT autour de la génération d'un meilleur code pour les structures > 4 octets sur 32 bits depuis nos discussions l'année dernière

Je n'en connais aucun.

que pensez-vous de la proposition de

Les types de framework doivent être des choix simples qui fonctionnent bien pour 95%+ des cas. Ce ne sont peut-être pas les plus rapides, mais c'est bien. Avoir le choix entre Hash32 et Hash64 n'est pas un choix simple.

Ça me va. Mais pouvons-nous au moins avoir une solution suffisamment bonne pour ces 95 % de cas ? Pour l'instant il n'y a rien... :-/

hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(this.s);

@CyrusNajmabadi Pourquoi

Pour les non-structs : afin que nous n'ayons pas besoin de vérifier null.

C'est proche de ce que nous générons également pour les types anonymes dans les coulisses. J'optimise le cas des valeurs non nulles connues pour générer du code qui plairait plus aux utilisateurs. Mais ce serait bien d'avoir juste une API intégrée pour cela.

L'appel à EqualityComparer.Default.GetHashCode est comme 10x+ plus cher que la vérification de null... .

L'appel à EqualityComparer.Default.GetHashCode est comme 10x+ plus cher que la vérification de null ..

Cela ressemble à un problème. si seulement il y avait une bonne API de code de hachage, nous pourrions appeler le Fx auquel je pourrais m'en remettre :)

(de plus, nous avons ce problème dans nos types anonymes car c'est également ce que nous générons là-bas).

Je ne sais pas ce que nous faisons pour les tuples, mais je suppose que c'est similaire.

Je ne sais pas ce que nous faisons pour les tuples, mais je suppose que c'est similaire.

System.Tuple passe par EqualityComparer<Object>.Default pour des raisons historiques. System.ValueTuple appelle Object.GetHashCode avec une vérification nulle - https://github.com/dotnet/coreclr/blob/master/src/mscorlib/shared/System/ValueTuple.cs#L809.

Oh non. On dirait que tuple peut simplement utiliser "HashHelpers". Cela pourrait-il être exposé afin que les utilisateurs puissent obtenir le même avantage ?

Super. Je suis heureux de faire quelque chose de similaire. J'ai commencé à partir de nos types anonymes parce que je pensais qu'il s'agissait de meilleures pratiques raisonnables. Si non, c'est bien. :)

Mais ce n'est pas pour ça que je suis ici. Je suis ici pour obtenir un système qui combine efficacement les hachages. Si/quand cela peut être fourni, nous passerons volontiers à l'appel au lieu de coder en dur en nombres aléatoires et de combiner nous-mêmes les valeurs de hachage.

Quelle serait la forme d'API qui, selon vous, fonctionnerait le mieux pour le code généré par le compilateur ?

Littéralement, n'importe laquelle des solutions 32 bits qui ont été présentées plus tôt me conviendrait. Heck, les solutions 64 bits me conviennent. Juste une sorte d'API que vous pouvez obtenir et qui dit "je peux combiner des hachages d'une manière raisonnable et produire un résultat raisonnablement distribué".

Je ne peux pas concilier ces déclarations:

Nous avions une structure HashCode immuable de 4 octets. Il avait une méthode Combine(int), qui mélangeait le code de hachage fourni avec son propre code de hachage via un algorithme de type DJBX33X, et renvoyait un nouveau HashCode.

@jkotas ne pensait pas que l'algorithme de type DJBX33X était assez robuste.

Et

Les types de framework doivent être des choix simples qui fonctionnent bien pour 95%+ des cas.

Ne pouvons-nous pas proposer un simple hachage d'accumulation de 32 bits qui fonctionne assez bien dans 95% des cas ? Quels sont les cas qui ne sont pas bien traités ici, et pourquoi pensons-nous qu'ils sont dans le cas à 95 % ?

@jkotas , la performance est-elle vraiment si critique pour ce type ? Je pense qu'en moyenne des choses comme les recherches de table de hachage et cela prendrait beaucoup plus de temps que quelques copies de structure. Si cela s'avère être un goulot d'étranglement, serait-il raisonnable de demander à l'équipe JIT d'optimiser les copies de structure 32 bits après la publication de l'API afin qu'elles soient incitées, plutôt que de bloquer cette API lorsque personne ne travaille sur l'optimisation copies?

Ne pouvons-nous pas proposer un simple hachage d'accumulation de 32 bits qui fonctionne assez bien dans 95% des cas ?

Nous avons été très gravement brûlés par défaut en accumulant le hachage 32 bits pour les chaînes, et c'est pourquoi le hachage Marvin pour les chaînes dans .NET Core - https://github.com/dotnet/corert/blob/87e58839d6629b5f90777f886a2f52d7a99c076f/src/System.Private.CoreLib src/System/Marvin.cs#L25. Je ne pense pas que nous voulions répéter la même erreur ici.

@jkotas , la performance est-elle vraiment si critique pour ce type ?

Je ne pense pas que la performance soit critique. Puisqu'il semble que cette API va être utilisée par du code de compilateur généré automatiquement, je pense que nous devrions préférer un code généré plus petit à son apparence. Le modèle non fluide est un code plus petit.

Nous avons été très gravement brûlés par défaut en accumulant le hachage 32 bits pour la chaîne

Cela ne semble pas être le cas à 95%. Nous parlons de développeurs normaux qui veulent juste un hachage "assez bon" pour tous ces types où ils font des choses manuellement aujourd'hui.

Puisqu'il semble que cette API va être utilisée par du code de compilateur généré automatiquement, je pense que nous devrions préférer un code généré plus petit à son apparence. Le modèle non fluide est un code plus petit.

Ceci n'est pas destiné à être utilisé par le compilateur Roslyn. Ceci est destiné à être utilisé par l'IDE Roslyn lorsque nous aidons les utilisateurs à générer des GetHashCodes pour leurs types. C'est un code que l'utilisateur verra et devra maintenir, et ayant quelque chose de sensé comme :

```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;

Je veux dire, nous avons déjà ce code dans le Fx :

https://github.com/dotnet/roslyn/blob/master/src/Compilers/Test/Resources/Core/NetFX/ValueTuple/ValueTuple.cs#L5

Nous pensons que c'est assez bon pour les tuples. Je ne comprends pas pourquoi ce serait un tel problème de le rendre disponible pour les utilisateurs qui le souhaitent pour leurs propres types.

Remarque : nous avons même envisagé de le faire à Roslyn :

c# return (this.A, this.B, this.C).GetHashCode();

Mais maintenant, vous obligez les gens à générer une structure (potentiellement grande) juste pour obtenir une sorte de comportement de hachage par défaut raisonnable.

Nous parlons de développeurs normaux qui veulent juste un hachage "assez bon" pour tous ces types où ils font des choses manuellement aujourd'hui.

Le hachage de chaîne d'origine était un hachage "assez bon" qui fonctionnait bien pour les développeurs normaux. Mais ensuite, il a été découvert que les serveurs Web ASP.NET étaient vulnérables aux attaques DoS, car ils ont tendance à stocker les éléments reçus dans des tables de hachage. Ainsi, le hachage « assez bon » s'est essentiellement transformé en un mauvais problème de sécurité.

Nous pensons que c'est assez bon pour les tuples

Non forcément. Nous avons fait une mesure d'arrêt pour les tuples afin de rendre le code de hachage aléatoire, ce qui nous donne la possibilité de modifier l'algorithme plus tard.

     return Hash.Combine(this.A?.GetHashCode() ?? 0,
                         this.B?.GetHashCode() ?? 0,
                         this.C?.GetHashCode() ?? 0);

Cela me semble raisonnable.

Je ne comprends pas votre position. Vous semblez dire deux choses :

Le hachage de chaîne d'origine était un hachage "assez bon" qui fonctionnait bien pour les développeurs normaux. Mais ensuite, il a été découvert que les serveurs Web ASP.NET étaient vulnérables aux attaques DoS, car ils ont tendance à stocker les éléments reçus dans des tables de hachage. Ainsi, le hachage « assez bon » s'est essentiellement transformé en un mauvais problème de sécurité.

Ok, si c'est le cas, alors fournissons un code de hachage qui convient aux personnes qui ont des problèmes de sécurité/DoS.

Les types de framework doivent être des choix simples qui fonctionnent bien pour 95%+ des cas.

Ok, si c'est le cas, alors fournissons un code de hachage qui est assez bon pour les 95% des cas. Les personnes qui ont des problèmes de sécurité/DoS peuvent utiliser les formulaires spécialisés qui sont documentés à cette fin.

Non forcément. Nous avons fait une mesure d'arrêt pour les tuples afin de rendre le code de hachage aléatoire, ce qui nous donne la possibilité de modifier l'algorithme plus tard.

D'accord. Pouvons-nous exposer cela afin que les utilisateurs puissent utiliser ce même mécanisme.

--
J'ai vraiment du mal ici parce qu'on dirait que nous disons "parce que nous ne pouvons pas trouver de solution universelle, tout le monde doit lancer la sienne". Cela semble être l'un des pires endroits où être. Parce que la plupart de nos clients ne pensent certainement pas à lancer leur propre "marvin hash" pour des problèmes de DoS. Ils ne font qu'ajouter, extraire ou combiner mal les hachages de champ en un hachage final.

Si nous nous soucions du cas à 95%, alors nous devrions juste faire un hachage généralement assez bon. SI nous nous soucions du cas des 5%, nous pouvons fournir une solution spécialisée pour cela.

Cela me semble raisonnable.

Super :) Pouvons-nous alors exposer :

```c#
espace de noms System.Numerics.Hashing
{
classe statique interne 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);

Cela aurait l'avantage d'être vraiment "assez bon" pour la grande majorité des cas, tout en guidant les gens sur la bonne voie d'initialisation avec des valeurs aléatoires afin qu'ils ne prennent pas de dépendances sur des hachages non aléatoires.

Les personnes qui ont des problèmes de sécurité/DoS peuvent utiliser les formulaires spécialisés qui sont documentés à cette fin.

Chaque application ASP.NET a des problèmes de sécurité/DoS.

Super :) Pouvons-nous alors exposer :

C'est différent de ce que j'ai dit est raisonnable.

Que pensez-vous de https://github.com/aspnet/Common/blob/dev/shared/Microsoft.Extensions.HashCodeCombiner.Sources/HashCodeCombiner.cs . C'est ce qui est utilisé en interne dans ASP.NET à plusieurs endroits aujourd'hui, et c'est ce dont je serais assez satisfait (sauf que la fonction de combinaison doit être plus forte - des détails d'implémentation que nous pouvons continuer à peaufiner).

@jkotas j'ai entendu ça :p

Donc, le problème ici est que les développeurs ne savent pas quand ils sont sensibles aux attaques DoS, car ce n'est pas quelque chose qu'ils pensent, c'est pourquoi nous avons changé les chaînes pour utiliser Marvin32.

Nous ne devons pas nous engager dans la voie du « 95% des cas n'ont pas d'importance », car nous n'avons aucun moyen de le prouver, et nous devons pécher par excès de prudence même lorsque cela a un coût en termes de performances. Si vous envisagez de vous éloigner de cela, la mise en œuvre du code de hachage nécessite un examen par Crypto Board, et pas seulement nous décidions « Cela semble assez bon ».

Chaque application ASP.NET a des problèmes de sécurité/DoS.

D'accord. Alors, comment gérez-vous le problème aujourd'hui selon lequel personne n'a d'aide avec les hashcodes et fait donc probablement les choses mal? Il est clair qu'il est acceptable d'avoir cet état du monde. Alors, qu'est-ce qui nuit à fournir un système de hachage raisonnable qui fonctionne probablement mieux que ce que les gens roulent à la main aujourd'hui ?

car nous n'avons aucun moyen de le prouver, et nous devons pécher par excès de prudence même lorsque cela a un coût de performance

Si vous ne fournissez pas quelque chose, les gens continueront à mal faire les choses. Le rejet du « assez bien » parce qu'il n'y a rien de parfait signifie simplement le mauvais statu quo que nous avons aujourd'hui.

Chaque application ASP.NET a des problèmes de sécurité/DoS.

Pouvez-vous expliquer cela? Si je comprends bien, vous avez un problème de DoS si vous acceptez des entrées arbitraires et que vous les stockez ensuite dans une structure de données qui fonctionne mal si les entrées peuvent être spécialement conçues. D'accord, je comprends à quel point c'est un problème avec les chaînes que l'on obtient dans les scénarios Web provenant de l'utilisateur.

Alors, comment cela s'applique-t-il aux autres types qui ne sont pas utilisés dans ce scénario ?

Nous avons ces ensembles de types :

  1. Types d'utilisateurs qui doivent être protégés contre le DoS. À l'heure actuelle, nous ne fournissons rien pour aider, nous sommes donc déjà dans une mauvaise passe car les gens ne font probablement pas ce qu'il faut.
  2. Types d'utilisateurs qui n'ont pas besoin d'être protégés contre le DoS. À l'heure actuelle, nous ne fournissons rien pour aider, nous sommes donc déjà dans une mauvaise passe car les gens ne font probablement pas ce qu'il faut.
  3. Types de framework qui doivent être sécurisés contre le DoS. À l'heure actuelle, nous les avons sécurisés DoS, mais via les API, nous ne les exposons pas.
  4. Types de framework qui n'ont pas besoin d'être sécurisés contre le DoS. Pour le moment, nous leur avons donné des hachages, mais via les API, nous ne les exposons pas.

Fondamentalement, nous pensons que ces cas sont importants, mais pas assez importants pour fournir réellement une solution aux utilisateurs pour gérer '1' ou '2'. Parce que nous craignons qu'une solution pour '2' ne soit pas bonne pour '1', nous ne la fournirons même pas en premier lieu. Et si nous ne sommes même pas disposés à fournir une solution pour '1', nous avons l'impression d'être dans une position incroyablement étrange. Nous sommes inquiets pour le DoSing et l'ASP, mais pas assez pour aider les gens. Et parce que nous n'aiderons pas les gens avec ça, nous ne sommes même pas disposés à aider avec les cas non DoS.

--

Si ces deux cas sont importants (ce que je suis prêt à accepter), alors pourquoi ne pas simplement donner deux API ? Documentez-les. Expliquez-leur clairement à quoi ils servent. Si les gens les utilisent correctement, grande. Si les gens ne les utilisent pas correctement, c'est toujours bien. Après tout, ils ne font probablement pas les choses correctement aujourd'hui de toute façon , alors comment les choses sont-elles pires ?

Qu'en pensez-vous

Je n'ai pas d'opinion dans un sens ou dans l'autre. S'il s'agit d'une API que les clients peuvent utiliser, qui fonctionne de manière acceptable et qui fournit une API simple avec un code clair de leur côté, alors je pense que c'est bien.

Je pense qu'il serait bien d'avoir un formulaire statique simple qui gère le cas à 99% de vouloir combiner un ensemble de champs/propriétés de manière ordonnée. Il semble qu'une telle chose pourrait être ajoutée à ce type assez simplement.

Je pense que ce serait bien d'avoir une forme statique simple

Se mettre d'accord.

Je pense qu'il serait bien d'avoir un formulaire statique simple qui gère le cas à 99% de vouloir combiner un ensemble de champs/propriétés de manière ordonnée. Il semble qu'une telle chose pourrait être ajoutée à ce type assez simplement.

Se mettre d'accord.

Je suis prêt à vous rencontrer tous les deux à mi-chemin sur celui-ci parce que je veux vraiment voir une sorte d'API émerger. @jkotas Je ne comprends toujours pas que vous soyez opposé à l'ajout d'une API basée sur des instances immuables ; vous avez d'abord dit que c'était parce que les copies 32 bits seraient lentes, ensuite parce que l'API mutable serait plus laconique (ce qui n'est pas vrai ; h.Combine(a).Combine(b) (version immuable) est plus courte que h.Combine(a); h.Combine(b); (mutable version)).

Cela dit, je suis prêt à revenir sur :

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
}

Cela vous semble-t-il raisonnable ?

Je ne peux pas éditer mon message pour le moment, mais je viens de réaliser que toutes les méthodes ne peuvent pas accepter T. Dans ce cas, nous pouvons simplement avoir 8 surcharges acceptant tous les entiers et obliger l'utilisateur à appeler GetHashCode.

Si ces deux cas sont importants (ce que je suis prêt à accepter), alors pourquoi ne pas simplement donner deux API ? Documentez-les. Expliquez-leur clairement à quoi ils servent. Si les gens les utilisent correctement, tant mieux. Si les gens ne les utilisent pas correctement, c'est toujours bien. Après tout, ils ne font probablement pas les choses correctement aujourd'hui de toute façon, alors comment les choses sont-elles pires ?

Parce que les gens n'utilisent pas les choses correctement quand ils sont là. Prenons un exemple simple, XSS. Dès le début, même les formulaires Web avaient la possibilité d'encoder la sortie HTML. Cependant, les développeurs ne connaissaient pas le risque, ne savaient pas comment le faire correctement et n'ont découvert que trop tard, leur application a été publiée, et oups, maintenant leur cookie d'authentification a été levé.

Donner aux gens un choix de sécurité suppose qu'ils

  1. Connaître le problème.
  2. Comprendre quels sont les risques.
  3. Peut évaluer ces risques.
  4. Peut facilement découvrir la bonne chose à faire.

Ces hypothèses ne sont généralement pas valables pour la majorité des développeurs, ils ne découvrent le problème que lorsqu'il est trop tard. Les développeurs ne vont pas aux conférences sur la sécurité, ils ne lisent pas les livres blancs et ils ne comprennent pas les solutions. Ainsi, dans le scénario ASP.NET HashDoS, nous avons fait le choix pour eux, nous les avons protégés par défaut, car c'était la bonne chose à faire et cela a eu le plus grand impact. Cependant, nous ne l'avons appliqué qu'aux chaînes, et cela a laissé les personnes qui construisaient des classes personnalisées à partir des entrées de l'utilisateur dans une mauvaise position. Nous devons faire ce qu'il faut, et aider à protéger ces clients maintenant, et en faire la valeur par défaut, avoir une fosse de succès, pas d'échec. La conception d'API pour la sécurité n'est parfois pas une question de choix, mais d'aide à l'utilisateur, qu'il le sache ou non.

Un utilisateur peut toujours créer un hachage non axé sur la sécurité ; donc étant donné les deux options

  1. L'utilitaire de hachage par défaut n'est pas sensible à la sécurité ; l'utilisateur peut créer une fonction de hachage sensible à la sécurité
  2. L'utilitaire de hachage par défaut est sensible à la sécurité ; l'utilisateur peut créer une fonction de hachage personnalisée non liée à la sécurité

Alors la seconde est probablement meilleure ; et ce qui est suggéré n'aurait pas l'impact perf d'un hachage crypto complet ; donc c'est un bon compromis ?

L'une des questions récurrentes dans ces discussions a été de savoir quel algorithme est parfait pour tout le monde. Je pense qu'il est sûr de dire qu'il n'y a pas un seul algorithme parfait. Cependant, je ne pense pas que cela devrait nous empêcher de fournir quelque chose de mieux que du code comme ce que @CyrusNajmabadi a montré, qui a tendance à avoir une mauvaise entropie pour les entrées .NET courantes ainsi que d'autres bogues de hachage courants (comme perdre des données d'entrée ou être facilement réinitialisable).

J'aimerais proposer quelques options pour contourner le problème du "meilleur algorithme":

  1. Choix explicites : je prévois d'envoyer bientôt une proposition d'API pour une suite de hachages non cryptographiques (peut-être xxHash, Marvin32 et SpookyHash par exemple). Une telle API a une utilisation légèrement différente d'un type HashCode ou HashCodeHelper, mais pour des raisons de discussion, supposons que nous pouvons résoudre ces différences. Si nous utilisons cette API pour GetHashCode :

    • Le code généré est explicite sur ce qu'il fait - si Roslyn génère Marvin32.Create(); , il permet aux utilisateurs expérimentés de savoir ce qu'il a décidé de faire et ils peuvent facilement le changer pour un autre algorithme de la suite s'ils le souhaitent.

    • Cela signifie que nous n'avons pas à nous soucier des changements de rupture. Si nous commençons avec un algorithme non aléatoire/faible entropie/lent, nous pouvons simplement mettre à jour Roslyn pour commencer à générer autre chose dans le nouveau code. L'ancien code continuera à utiliser l'ancien hachage et le nouveau code utilisera le nouveau hachage. Les développeurs (ou un correctif de code Roslyn) peuvent modifier l'ancien code s'ils le souhaitent.

    • Le plus gros inconvénient auquel je puisse penser est que certaines des optimisations que nous pourrions souhaiter pour GetHashCode pourraient être préjudiciables pour d'autres algorithmes. Par exemple, alors qu'un état interne de 32 bits fonctionne bien avec des structures immuables, un état interne de 256 bits dans (disons) CityHash peut faire perdre beaucoup de temps à la copie.

  1. Randomisation : commencez avec un algorithme correctement randomisé (le code que @CyrusNajmabadi a montré avec une valeur initiale aléatoire ne compte pas car il est probablement possible de supprimer le caractère aléatoire). Cela garantit que nous pouvons modifier l'implémentation sans problèmes de compatibilité. Nous devrons toujours être très sensibles aux changements de performances si nous modifions l'algorithme. Cependant, ce serait également un avantage potentiel, car nous pourrions faire des choix par architecture (ou même par périphérique). Par exemple, ce site montre que xxHash est le plus rapide sur un Mac x64 tandis que SpookyHash est le plus rapide sur Xbox et iPhone. Si nous empruntons cette voie avec l'intention de changer les algorithmes à un moment donné, nous devrons peut-être penser à concevoir une API qui a toujours des performances raisonnables s'il y a un état interne de 64 bits ou plus.

CC @bartonjs , @terrajobst

@morganbr Il n'y a pas un seul algorithme parfait, mais je pense qu'avoir un algorithme, qui fonctionne assez bien la plupart du temps, exposé à l'aide d'une API simple et facile à comprendre est la chose la plus utile qui puisse être faite. Avoir une suite d'algorithmes en plus de cela, pour des utilisations avancées, c'est bien. Mais cela ne devrait pas être la seule option, je ne devrais pas avoir à apprendre qui est Marvin juste pour pouvoir mettre mes objets dans un Dictionary .

Je ne devrais pas avoir à apprendre qui est Marvin juste pour pouvoir mettre mes objets dans un dictionnaire.

J'aime la façon dont tu dis ça. J'aime aussi que vous ayez mentionné le dictionnaire lui-même. IDictionary est quelque chose qui peut avoir des tonnes d'impls différents avec toutes sortes de qualités différentes (voir les API de collections sur de nombreuses plateformes). Cependant, nous ne fournissons toujours qu'un « Dictionnaire » de base qui fait un travail décent dans l'ensemble, même s'il n'excelle pas dans toutes les catégories.

Je pense que c'est ce que beaucoup de gens recherchent dans une bibliothèque de hachage. Quelque chose qui fait le travail, même s'il n'est pas parfait pour tous les usages.

@morganbr Je pense que les gens veulent simplement un moyen d'écrire GetHashCode qui soit meilleur que ce qu'ils font aujourd'hui (généralement une combinaison d'opérations mathématiques qu'ils ont copiées à partir de quelque chose sur le Web). Si vous pouvez simplement fournir un impl de base qui fonctionne bien, alors les gens seront heureux. Vous pouvez alors disposer d'une API en coulisse pour les utilisateurs avancés s'ils ont un besoin important de fonctions de hachage spécifiques .

En d'autres termes, les personnes qui écrivent des hashcodes aujourd'hui ne sauront pas ou ne se soucieront pas de savoir pourquoi elles voudraient Spooky vs Marvin vs Murmur. Seule une personne qui a un besoin particulier pour l'un de ces codes de hachage spécifiques irait chercher. Mais beaucoup de gens ont besoin de dire "voici l'état de mon objet, fournissez-moi un moyen de produire un hachage bien distribué qui est rapide que je peux ensuite utiliser avec des dictionnaires, et qui, je suppose, m'empêche d'être DOS si j'arrive pour prendre des entrées non fiables, les hacher et les stocker".

@CyrusNajmabadi Le problème est que si nous étendons nos notions actuelles de compatibilité dans le futur, nous constatons qu'une fois ce type expédié, il ne peut plus jamais changer (à moins que nous ne constations que l'algorithme est horriblement cassé de manière "il rend toutes les applications attaquables" ).

Une fois peut affirmer que si cela commence de manière stable et aléatoire, il devient facile de modifier l'implémentation, car vous ne pouvez de toute façon pas dépendre de la valeur d'une exécution à l'autre. Mais si quelques années plus tard, nous découvrons qu'il existe un algorithme qui fournit un équilibrage aussi bon sinon meilleur des seaux de hachage avec de meilleures performances dans le cas général, mais crée une structure impliquant une liste\

Selon la suggestion de Morgan, le code que vous écrivez aujourd'hui aura effectivement les mêmes caractéristiques de performance pour toujours. Pour les applications qui auraient pu s'améliorer, c'est dommage. Pour les applications qui auraient empiré, c'est fantastique. Mais lorsque nous trouvons le nouvel algorithme, nous l'enregistrons et nous modifions Roslyn (et suggérons un changement pour ReSharper/etc) pour commencer à générer des choses avec NewAwesomeThing2019 au lieu de SomeThingThatWasConsidedAwesomeIn2018.

Tout ce qui est super boîte noire comme celui-ci ne peut être fait qu'une seule fois. Et puis nous sommes coincés avec ça pour toujours. Ensuite, quelqu'un écrit le suivant, qui a de meilleures performances moyennes, il y a donc deux implémentations de boîte noire dont vous ne savez pas pourquoi vous choisiriez entre elles. Et puis... et puis... .

Donc, bien sûr, vous ne savez peut-être pas pourquoi Roslyn/ReSharper/etc a écrit automatiquement GetHashCode pour vous en utilisant Marvin32, ou Murmur, ou FastHash, ou une combinaison/conditionnelle basée sur IntPtr.Size. Mais vous avez le pouvoir de l'examiner. Et vous avez le pouvoir de le changer sur vos types plus tard, au fur et à mesure que de nouvelles informations sont révélées... mais nous vous avons également donné le pouvoir de le garder inchangé. (Ce serait triste si nous écrivions ceci, et dans 3 ans Roslyn/ReSharper/etc évitent explicitement de l'appeler, parce que le nouvel algorithme est tellement meilleur... Habituellement).

@bartonjs Qu'est-ce qui rend le hachage différent de tous les endroits où .Net vous fournit un algorithme de boîte noire ou une structure de données ? Par exemple, le tri (introsort), Dictionary (chaînage séparé basé sur un tableau), StringBuilder (liste chaînée de 8k morceaux), la plupart de LINQ.

Nous avons examiné cela de plus près aujourd'hui. Toutes nos excuses pour le retard et les allers-retours sur cette question.

Conditions

  • A qui s'adresse l'API ?

    • L'API n'a pas besoin de produire un hachage cryptographique fort

    • Mais : l'API doit être suffisamment bonne pour que nous puissions l'utiliser dans le framework lui-même (par exemple dans la BCL et ASP.NET)

    • Cependant, cela ne signifie pas que nous devons utiliser l'API partout. Ce n'est pas grave s'il y a des parties du FX où nous voulons en utiliser une personnalisée soit pour des risques de sécurité/DOS ou à cause des performances. Des exceptions existeront toujours .

  • Quelles sont les propriétés souhaitées de ce hachage ?

    • Tous les bits de l'entrée sont utilisés

    • Le résultat est bien réparti

    • L'API fournira "un" code de hachage, mais ne garantira pas un algorithme de code de hachage particulier. Cela nous permet d'utiliser un algorithme différent plus tard ou d'utiliser des algorithmes différents sur des architectures différentes.

    • L'API garantira que dans un processus donné, les mêmes valeurs produiront le même code de hachage. Différentes instances de la même application produiront probablement des codes de hachage différents en raison de la randomisation. Cela nous permet de garantir que les consommateurs ne peuvent pas conserver les valeurs de hachage et s'appuyer accidentellement sur leur stabilité à travers les exécutions (ou pire, les versions de la plate-forme).

Forme API

```C#
// vivra dans l'assemblage de base
// .NET Framework : mscorlib
// .NET Core : System.Runtime / System.Private.CoreLib
système d'espace de noms
{
structure publique HashCode
{
public statique int Combiner(valeur T11) ;
public statique int Combiner(valeur T11, valeur T22) ;
public statique int Combiner(valeur T11, valeur T22, valeur T33) ;
public statique int Combiner(valeur T11, valeur T22, valeur T33, valeur T44) ;
public statique int Combiner(valeur T11, valeur T22, valeur T33, valeur T44, valeur T55) ;
public statique int Combiner(valeur T11, valeur T22, valeur T33, valeur T44, valeur T55, valeur T66) ;
public statique int Combiner(valeur T11, valeur T22, valeur T33, valeur T44, valeur T55, valeur T66, valeur T77) ;
public statique int Combiner(valeur T1, valeur T22, valeur T33, valeur T44, valeur T55, valeur T66, valeur T77, valeur T88) ;

    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);
}

Le cas le plus compliqué est celui où le développeur doit modifier la façon dont le hachage est calculé. L'idée est que le site d'appel passe le hachage souhaité plutôt que l'objet/la valeur, comme ceci :

```C#
classe partielle publique Client
{
public override int GetHashCode() =>
HashCode.Combine(
Identifiant,
StringComparer.OrdinalIgnoreCase.GetHashCode(FirstName),
StringComparer.OrdinalIgnoreCase.GetHashCode(Nom),
);
}

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();
    }
}

Prochaines étapes

Cette question restera d'actualité. Afin de mettre en œuvre l'API, nous devons décider quel algorithme utiliser.

@morganbr fera une proposition pour les bons candidats. De manière générale, nous ne voulons pas écrire un algorithme de hachage à partir de zéro - nous voulons en utiliser un bien connu dont les propriétés sont bien comprises.

Cependant, nous devons mesurer l'implémentation pour les charges de travail .NET typiques et voir quel algorithme produit de bons résultats (débit et distribution). Il est probable que les réponses diffèrent selon l'architecture du processeur, nous devrions donc en tenir compte lors de la mesure.

@jamesqo , êtes-vous toujours intéressé à travailler dans ce domaine ? Dans ce cas, veuillez mettre à jour la proposition en conséquence.

@terrajobst , nous pourrions également vouloir public static int Combine<T1>(T1 value); . Je sais que cela a l'air un peu drôle, mais cela fournirait un moyen de diffuser des bits de quelque chose avec un espace de hachage d'entrée limité. Par exemple, de nombreuses énumérations n'ont que quelques hachages possibles, n'utilisant que les derniers bits du code. Certaines collections sont construites sur l'hypothèse que les hachages sont répartis sur un plus grand espace, donc la diffusion des bits peut aider la collection à fonctionner plus efficacement.

public void Add(string value, StrinComparison comparison);

Nit : Le paramètre StringComparison doit être nommé comparisonType pour correspondre au nom utilisé partout ailleurs. StringComparison est utilisé comme paramètre.

Les critères qui nous aideraient à choisir les algorithmes seraient :

  1. L'algorithme a-t-il un bon effet d'avalanche ? C'est-à-dire, chaque bit d'entrée a-t-il 50% de chances de renverser chaque bit de sortie ? Ce site a une étude de plusieurs algorithmes populaires.
  2. L'algorithme est-il rapide pour les petites entrées ? Étant donné que HashCode.Combine gère généralement 8 entiers ou moins, le temps de démarrage peut être plus important que le débit. Ce site a un ensemble intéressant de données pour commencer. C'est également là que nous pouvons avoir besoin de réponses différentes pour différentes architectures ou d'autres pivots (OS, AoT vs JIT, etc.).

Ce que nous aimerions vraiment voir, ce sont des chiffres de performance pour les candidats écrits en C# afin que nous puissions être raisonnablement confiants que leurs caractéristiques tiendront pour .NET. Si vous écrivez un candidat et que nous ne le choisissons pas pour cela, ce sera toujours un travail utile chaque fois que je réunirai la proposition d'API pour l'API de hachage non cryptographique.

Voici quelques candidats qui méritent d'être évalués (mais n'hésitez pas à en proposer d'autres) :

  • Marvin32 (nous avons déjà une implémentation C# ici ). Nous savons que c'est assez rapide pour String.GetHashCode et nous pensons qu'il est résistant au HashDoS
  • xxHash32 (le plus rapide sur l'algorithme sur x86 ici qui a une qualité supérieure selon SMHasher)
  • FarmHash (le plus rapide sur x64 ici . Je n'ai pas trouvé un bon indicateur de qualité pour cela. Celui-ci pourrait être difficile à écrire en C# cependant)
  • xxHash64 (tronqué à 32 bits) (Ce n'est pas un gain de vitesse clair, mais cela pourrait être facile à faire si nous avons déjà xxHash32)
  • SpookyHash (A tendance à bien fonctionner sur des ensembles de données plus volumineux)

Dommage que les méthodes Add ne puissent pas avoir un type de retour de ref HashCode et retourner ref this afin qu'elles puissent être utilisées de manière fluide,

Les retours de readonly ref permettraient-ils ? /cc @jaredpar @VSadov

AVERTISSEMENT : si quelqu'un choisit une implémentation de hachage à partir d'une base de code existante quelque part sur Internet, veuillez conserver le lien vers la source et vérifier la licence (nous devrons également le faire).

Si la licence n'est pas compatible, nous devrons peut-être écrire l'algorithme à partir de zéro.

OMI, l'utilisation des méthodes Add devrait être extrêmement rare. Ce sera pour des scénarios très avancés, et le besoin de pouvoir être « fluide » ne sera pas vraiment là.

Pour les cas d'utilisation courants pour 99% de tous les cas de code utilisateur, on devrait pouvoir utiliser => HashCode.Combine(...) et tout ira bien.

@morganbr

nous pourrions également vouloir public static int Combine<T1>(T1 value); . Je sais que cela a l'air un peu drôle, mais cela fournirait un moyen de diffuser des bits de quelque chose avec un espace de hachage d'entrée limité

Ayez du sens. Je l'ai ajouté.

@justinvp

Nit : Le paramètre StringComparison doit être nommé comparisonType pour correspondre au nom utilisé partout ailleurs. StringComparison est utilisé comme paramètre.

Fixé.

@CyrusNajmabadi

OMI, l'utilisation des méthodes Add devrait être extrêmement rare. Ce sera pour des scénarios très avancés, et le besoin de pouvoir être « fluide » ne sera pas vraiment là.

D'accord.

@benaadams - re: ref retournant this de Add - non, this ne peut pas être retourné par ref dans les méthodes struct car il peut s'agir d'un rValue ou d'un temp.

```C#
ref var r = (nouveau T()).ReturnsRefThis();

// r fait ici référence à une variable. Lequel? Quelle est la portée/durée de vie ?
r = Autre chose();
```

Au cas où cela serait utile à des fins de comparaison, il y a quelques années, j'ai porté la fonction de hachage source C ) vers C# ici .

Je m'interroge sur les collections :

@terrajobst

c# public void Add<T>(T[] value);

Pourquoi y a-t-il une surcharge pour les tableaux, mais pas pour les collections générales (c'est- IEnumerable<T> dire

De plus, ne va-t-il pas être déroutant que HashCode.Combine(array) et hashCode.Add((object)array) se comportent dans un sens (utilisez l'égalité de référence) et que hashCode.Add(array) se comportent d'une autre manière (combine les codes de hachage des valeurs dans le tableau) ?

@CyrusNajmabadi

Pour les cas d'utilisation courants pour 99% de tous les cas de code utilisateur, on devrait pouvoir simplement utiliser => HashCode.Combine(...) et tout ira bien.

Si l'objectif est vraiment de pouvoir utiliser Combine dans 99% des cas d'utilisation (et non, disons, 80%), alors Combine ne devrait-il pas prendre en charge les collections de hachage basées sur les valeurs dans la collection ? Peut-être qu'il devrait y avoir une méthode distincte qui fait cela (soit une méthode d'extension, soit une méthode statique sur HashCode ) ?

Si Add est un scénario de puissance, devons-nous supposer que l'utilisateur doit choisir entre Object.GetHashCode et combiner des éléments individuels de collections ? Si cela pouvait aider, nous pourrions envisager de renommer les versions du tableau (et les versions potentielles de IEnumerable). Quelque chose comme:
c# public void AddEnumerableHashes<T>(IEnumerable<T> enumerable); public void AddEnumerableHashes<T>(T[] array); public void AddEnumerableHashes<T>(T[] array, int index, int length);
Je me demande si nous aurions également besoin de surcharges avec IEqualityComparers.

Proposition : Faire en sorte que la structure du générateur implémente IEnumerable pour prendre en charge la syntaxe de l'initialiseur de collection :

C# return new HashCode { SomeField, OtherField, { SomeString, StringComparer.UTF8 }, { SomeHashSet, HashSet<int>.CreateSetComparer() } }.GetHashCode()

C'est beaucoup plus élégant que d'appeler Add() à la main (en particulier, vous n'avez pas besoin d'une variable temporaire), et n'a toujours pas d'allocations.

plus de détails

@SLaks Peut-être que cette syntaxe plus agréable pourrait attendre https://github.com/dotnet/csharplang/issues/455 (en supposant que cette proposition ait un support), afin que HashCode n'ait pas à implémenter de faux IEnumerable ?

Nous avons décidé de ne pas surcharger GetHashCode() pour produire le code de hachage car cela serait étrange, tant du point de vue du nom que du point de vue comportemental (GetHashCode() devrait renvoyer le code de hachage de l'objet, pas celui en cours de calcul).

Je trouve étrange que GetHashCode ne retourne pas le hashcode calculé. Je pense que cela va embrouiller les développeurs. Par exemple, @SLaks l'a déjà utilisé dans sa proposition au lieu d'utiliser ToHashCode .

@justinvp Si GetHashCode() ne retourne pas le code de hachage calculé, il devrait probablement être marqué [Obsolete] et [EditorBrowsable(Never)] .

D'un autre côté, je ne vois pas l'inconvénient de renvoyer le code de hachage calculé.

@terrajobst

Nous avons décidé de ne pas surcharger GetHashCode() pour produire le code de hachage car ce serait étrange, tant du point de vue du nom que du point de vue comportemental ( GetHashCode() devrait renvoyer le code de hachage de l'objet, pas celui en cours de calcul).

Oui, GetHashCode() doit renvoyer le code de hachage de l'objet, mais y a-t-il une raison pour laquelle les deux codes de hachage doivent être différents ? C'est toujours correct, puisque deux instances de HashCode avec le même état interne renverront la même valeur à partir de GetHashCode() .

@terrajobst je viens de voir votre commentaire. Excusez-moi pour la réponse tardive, j'ai mis du temps à examiner la notification car je pensais que ce ne serait que plus de va-et-vient qui n'allait nulle part. Content de voir que ce n'est pas le cas ! :tada:

Je serais ravi de prendre cela et de mesurer le débit/la distribution (je suppose que c'est ce que vous vouliez dire par "intéressé à travailler dans ce domaine"). Donnez-moi une seconde pour finir de lire tous les commentaires ici, cependant.

@terrajobst

Pouvons-nous changer

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 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);

? J'ai renommé Add -> AddRange pour éviter le comportement mentionné par @svick . J'ai supprimé les surcharges byte car nous pouvons nous spécialiser en utilisant typeof(T) == typeof(byte) à l'intérieur de la méthode si nous devons faire quelque chose de spécifique à un octet. Aussi, j'ai changé value -> values et length -> count . Il est également logique d'avoir une surcharge de comparateur.

@terrajobst Pouvez-vous me rappeler pourquoi

        public void Add(string value);
        public void Add(string value, StringComparison comparisonType);

est nécessaire quand on a

        public void Add<T>(T value);
        public void Add<T>(T value, IEqualityComparer<T> comparer);

?

@svick

@justinvp Si GetHashCode() ne retourne pas le code de hachage calculé, il devrait probablement être marqué [Obsolete] et [EditorBrowsable(Never)].

:+1:

@terrajobst Pouvons-nous revenir à une conversion implicite de HashCode -> int , donc pas de méthode ToHashCode ? edit : ToHashCode c'est bien. Voir la réponse de @CyrusNajmabadi ci-dessous.

@jamesqo StringComparison est une énumération.
Cependant, les gens pourraient utiliser l'équivalent StringComparer place.

Pouvons-nous revenir à une conversion implicite de HashCode -> int, donc pas de méthode ToHashCode ?

Nous en avons discuté et avons décidé de ne pas le faire lors de la réunion. Le problème est que lorsque l'utilisateur obtient le « int » final, ce travail supplémentaire est souvent effectué. c'est-à-dire que les composants internes du hashcode effectueront souvent une étape de finalisation et peuvent se réinitialiser à un nouvel état. Que cela se produise avec une conversion implicite serait étrange. Si vous avez fait ceci :

HashCode hc = ...

int i1 = hc;
int i2 = hc;

Ensuite, vous pourriez obtenir des résultats différents.

Pour cette raison, nous n'aimons pas non plus la conversion explicite (car les gens ne considèrent pas les conversions comme un changement d'état interne).

Avec une méthode, nous pouvons documenter explicitement que cela se produit. On peut même potentiellement le nommer pour le véhiculer autant. c'est-à-dire "ToHashCodeAndReset" (bien que nous ayons décidé de ne pas le faire). Mais au moins la méthode peut avoir une documentation claire sur elle que l'utilisateur peut voir dans des choses comme intellisense. Ce n'est pas vraiment le cas avec les conversions.

J'ai supprimé les surcharges d'octets car nous pouvons nous spécialiser en utilisant typeof(T) == typeof(byte)

L'IIRC craignait que cela ne soit pas correct du point de vue de l'ECE. Mais cela n'a peut-être été que pour les cas "typeof()" sans valeur. Tant que le jit fera effectivement ce qu'il faut pour les cas typeof() de type valeur, alors cela devrait être bon.

@CyrusNajmabadi Je int pouvait impliquer un état de mutation. ToHashCode c'est alors.

Pour ceux qui pensent à la perspective crypto - http://tuprints.ulb.tu-darmstadt.de/2094/1/thesis.lehmann.pdf

@terrajobst , avez-vous eu le temps de lire mes commentaires (à partir d' ici ) et de décider si vous approuvez la forme de l'API modifiée ? Si c'est le cas, je pense que cela peut être marqué comme approuvé par l'API/à gagner et nous pouvons commencer à décider d'un algorithme de hachage.

@blowdart , une partie en particulier que vous voudriez souligner ?

Je n'ai peut-être pas été trop explicite à ce sujet ci-dessus, mais les seuls hachages non cryptographiques dans lesquels je ne connais pas les ruptures HashDoS sont Marvin et SipHash. C'est-à-dire que même l'ensemencement (disons) Murmur avec une valeur aléatoire peut toujours être rompu et utilisé pour un DoS.

Aucun, je viens de le trouver intéressant, et je pense que la documentation à ce sujet devrait dire "Ne pas utiliser sur les codes de hachage générés via des algorithmes cryptographiques".

Les décisions

  • Nous devrions supprimer toutes les méthodes AddRange car le scénario n'est pas clair. Il est peu probable que les tableaux apparaissent très souvent. Et une fois que de plus grands tableaux sont impliqués, la question est de savoir si le calcul doit être mis en cache. Voir la boucle for du côté appelant montre clairement que vous devez y penser.
  • Nous ne voulons pas non plus ajouter IEnumerable surcharges AddRange car elles seraient allouées.
  • Nous ne pensons pas avoir besoin de la surcharge de Add qui prend string et StringComparison . Oui, ceux-ci sont probablement plus efficaces que d'appeler via le IEqualityComparer , mais nous pouvons résoudre ce problème plus tard.
  • Nous pensons que marquer GetHashCode comme obsolète avec erreur est une bonne idée, mais nous irions un peu plus loin et nous nous cacherions également d'IntelliSense.

Cela nous laisse avec :

```C#
// vivra dans l'assemblage de base
// .NET Framework : mscorlib
// .NET Core : System.Runtime / System.Private.CoreLib
système d'espace de noms
{
structure publique HashCode
{
public statique int Combiner(valeur T11) ;
public statique int Combiner(valeur T11, valeur T22) ;
public statique int Combiner(valeur T11, valeur T22, valeur T33) ;
public statique int Combiner(valeur T11, valeur T22, valeur T33, valeur T44) ;
public statique int Combiner(valeur T11, valeur T22, valeur T33, valeur T44, valeur T55) ;
public statique int Combiner(valeur T11, valeur T22, valeur T33, valeur T44, valeur T55, valeur T66) ;
public statique int Combiner(valeur T11, valeur T22, valeur T33, valeur T44, valeur T55, valeur T66, valeur T77) ;
public statique int Combiner(valeur T1, valeur T22, valeur T33, valeur T44, valeur T55, valeur T66, valeur T77, valeur T88) ;

    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();
}

}
```

Prochaines étapes : le problème est à saisir -- pour implémenter l'API dont nous avons besoin avec plusieurs algorithmes candidats à titre d'expériences -- voir https://github.com/dotnet/corefx/issues/14354#issuecomment -305028686 pour la liste, afin que nous puissions décider quel algorithme utiliser (sur la base des mesures de débit et de distribution, réponse probablement différente selon l'architecture du processeur).

Complexité : Grande

Si quelqu'un est intéressé à le récupérer, veuillez nous contacter par ping. Il pourrait même y avoir de la place pour plusieurs personnes qui y travaillent ensemble. ( @jamesqo vous avez le choix prioritaire car vous avez investi le plus et le plus longtemps dans le problème)

@karelz Malgré mon commentaire ci - @morganbr et assez complexe , je ne peux donc pas la traduire facilement en C# pour tester par moi-même. J'ai peu d'expérience en C++, donc j'aurais aussi du mal à installer la bibliothèque et à écrire une application de test.

Cependant, je ne veux pas que cela reste sur la liste des candidats pour toujours. Si personne ne le prend dans une semaine à partir d'aujourd'hui, j'envisagerai de poster une question sur Programmers SE ou Reddit.

Je ne l'ai pas mis au banc (ni optimisé), mais voici une implémentation de base de l'algorithme de hachage Murmur3 que j'utilise dans plusieurs de mes projets personnels : https://gist.github.com/tannergooding/0a12559d1a912068b9aeb4b9586aad7f

Je pense que la solution la plus optimale ici va être de modifier dynamiquement l'algorithme de hachage en fonction de la taille des données d'entrée.

Ex : Mumur3 (et d'autres) sont très rapides pour les grands ensembles de données et offrent une excellente distribution, mais ils peuvent fonctionner « mal » (en termes de vitesse, pas de distribution) pour des ensembles de données plus petits.

J'imagine que nous devrions faire quelque chose comme : Si le nombre total d'octets est inférieur à X, faites l'algorithme A ; sinon, faites l'algorithme B. Ce sera toujours déterministe (par exécution), mais nous permettra de fournir une vitesse et une distribution en fonction de la taille réelle des données d'entrée.

Il convient également de noter que plusieurs des algorithmes mentionnés ont des implémentations spécialement conçues pour les instructions SIMD, donc une solution la plus performante impliquerait probablement un FCALL à un certain niveau (comme c'est le cas avec certaines des implémentations BufferCopy) ou peut impliquer de prendre une dépendance sur System.Numerics.Vector .

@jamesqo , nous sommes heureux de vous aider à faire des choix ; ce dont nous avons le plus besoin d'aide, ce sont les données de performances pour les implémentations candidates (idéalement C#, bien que, comme le souligne @tannergooding , certains algorithmes nécessitent une prise en charge spéciale du compilateur). Comme je l'ai mentionné ci-dessus, si vous construisez un candidat qui n'est pas choisi, nous l'utiliserons probablement plus tard, alors ne vous inquiétez pas du gaspillage de travail.

Je sais qu'il existe des références pour diverses implémentations, mais je pense qu'il est important d'avoir une comparaison en utilisant cette API et une gamme probable d'entrées (par exemple, des structures avec 1 à 10 champs).

@tannergooding , ce type d'adaptabilité est peut-être le plus performant, mais je ne vois pas comment cela fonctionnerait avec la méthode Add car elle ne sait pas combien de fois elle sera appelée. Bien que nous puissions le faire avec Combine, cela signifierait qu'une série d'appels Add pourrait produire un résultat différent de l'appel Combine correspondant.

De plus, étant donné que la plage d'entrées la plus probable est de Combine`1 octets ( Combine`8 ), j'espère qu'il n'y aura pas de grands changements de performances sur cette plage.

ce type d'adaptabilité est peut-être le plus performant, mais je ne vois pas comment cela fonctionnerait avec la méthode Add car elle ne sait pas combien de fois elle sera appelée.

Je ne suis pas personnellement convaincu que la forme de l'API soit tout à fait appropriée pour le hachage à usage général (elle est cependant proche) ...

Actuellement, nous exposons des méthodes Combine pour la construction statique. Si ceux-ci sont destinés à combiner toutes les entrées et à produire un code de hachage finalisé, le nom est « pauvre » et quelque chose comme Compute pourrait être plus approprié.

Si nous exposons des méthodes Combine , elles devraient simplement mélanger toutes les entrées et les utilisateurs devraient être obligés d'appeler une méthode Finalize qui prend la sortie de la dernière combinaison ainsi que le nombre total d'octets qui ont été combinés pour produire un code de hachage finalisé (la finalisation d'un code de hachage est importante car c'est ce qui provoque l'avalanche des bits).

Pour le modèle de générateur, nous exposons une méthode Add et ToHashCode . Il n'est pas clair si la méthode Add est destinée à stocker les octets et à ne combiner/finaliser que sur l'appel à ToHashCode (auquel cas nous pouvons choisir le bon algorithme dynamiquement) ou s'ils sont destiné à être combiné à la volée, il doit être clair que c'est le cas (et que l'implémentation doit suivre en interne la taille totale des octets combinés).

Pour ceux qui recherchent un point de départ moins compliqué, essayez xxHash32. C'est susceptible de se traduire assez facilement en C# (les gens l'ont fait ).

Je teste toujours localement, mais je vois les débits suivants pour mon implémentation C# de Murmur3.

Il s'agit des méthodes de combinaison statiques pour 1 à 8 entrées :

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

Mon implémentation suppose que GetHashCode doit être appelé pour chaque entrée et que la valeur calculée doit être finalisée avant d'être renvoyée.

J'ai combiné les valeurs int , car elles sont les plus simples à tester.

Pour calculer le débit, j'ai exécuté 10 001 itérations, en jetant la première itération en tant qu'exécution de "échauffement".

À chaque itération, j'exécute 10 000 sous-itérations où j'appelle HashCode.Combine , en passant le résultat de la sous-itération précédente comme première valeur d'entrée de l'itération suivante.

Je fais ensuite la moyenne de toutes les itérations pour obtenir le temps écoulé moyen, puis je divise ce chiffre par le nombre de sous-itérations exécutées par boucle pour obtenir le temps moyen par appel. Je calcule ensuite le nombre d'appels pouvant être effectués par seconde et je le multiplie par le nombre d'octets combinés pour calculer le débit réel.

Va nettoyer le code et le partager un peu.

@tannergooding , cela semble être un grand progrès. Pour vous assurer que vous obtenez les bonnes mesures, l'objectif de l'API est qu'un appel à HashCode.Combine(a, b) équivaut à appeler

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

Dans les deux cas, les données doivent être introduites dans le même état de hachage interne et le hachage doit être finalisé une fois à la fin.

??

C'est effectivement ce que fait le code que j'ai écrit. La seule différence est que j'inline effectivement tout le code (il n'est pas nécessaire d'allouer new HashCode() et de suivre le nombre d'octets combinés puisqu'il est constant).

@morganbr. Implémentation + Test de débit pour Murmur3 : https://gist.github.com/tannergooding/89bd72f05ab772bfe5ad3a03d6493650

MurmurHash3 est basé sur l'algorithme décrit ici : https://github.com/aappleby/smhasher/wiki/MurmurHash3 , le repo dit qu'il s'agit du MIT

Travailler sur xxHash32 (clause BSD-2 -- https://github.com/Cyan4973/xxHash/blob/dev/xxhash.c) et SpookyHash (domaine public -- http://www.burtleburtle.net/bob/hash /spooky.html) variantes

@tannergooding Encore une fois, ce n'est pas un expert en hachage mais je me suis souvenu [lire un article][1] qui disait que Murmur n'était pas résistant au DoS, donc il suffit de le souligner avant de choisir cela.

@jamesqo , je me trompe peut-être, mais je suis à peu près certain que cette vulnérabilité s'appliquait à Murmur2 et non à Murmur3.

Dans les deux cas, j'implémente plusieurs algorithmes afin que nous puissions obtenir des résultats de débit pour C#. La distribution et d'autres propriétés de ces algorithmes sont assez bien connues, nous pouvons donc choisir celui qui est le meilleur plus tard 😄

Oups, j'ai oublié de créer un lien vers l'article : http://emboss.github.io/blog/2012/12/14/breaking-murmur-hash-flooding-dos-reloaded/.

@tannergooding OK. Ça a l'air correct :+1:

@tannergooding , j'ai jeté un coup d'œil à votre implémentation Murmur3 et elle semble généralement correcte et probablement assez bien optimisée. Pour être sûr de bien comprendre, utilisez-vous le fait que CombineValue et l'état interne de Murmur sont tous deux de 32 bits ? C'est probablement une très bonne optimisation pour ce cas et explique une partie de ma confusion précédente.

Si nous devions l'adopter, il faudrait peut-être quelques ajustements (ils ne feront probablement pas une grande différence pour les mesures de performances) :

  • Combinerdevrait toujours appeler CombineValue sur value1
  • Les premiers appels CombineValue doivent prendre une graine aléatoire
  • ToHashCode devrait réinitialiser _bytesCombined et _combinedValue

En attendant, alors que j'aspire à cette API, à quel point est-ce mauvais pour moi d'implémenter GetHashCode via (field1, field2, field3).GetHashCode() ?

@jnm2 , le combineur de code de hachage ValueTuple a tendance à mettre vos entrées dans l'ordre dans le code de hachage (et à rejeter les moins récentes). Pour quelques champs et une table de hachage qui se divise par un nombre premier, vous ne le remarquerez peut-être pas. Pour beaucoup de champs ou une table de hachage qui divise par une puissance de deux, l'entropie du dernier champ que vous insérez aura le plus d'influence sur si vous avez des collisions (par exemple si votre dernier champ est un bool ou un petit int, vous 'aura probablement beaucoup de collisions, si c'est un guide, vous n'en aurez probablement pas).

ValueTuple ne fonctionne pas non plus bien avec les champs qui sont tous à 0.

En passant, j'ai dû arrêter de travailler sur d'autres implémentations (avoir un travail plus prioritaire). Je ne sais pas quand je pourrai le récupérer.

Donc, si ce n'est pas assez bon pour un type structuré, pourquoi est-ce assez bon pour un tuple ?

@jnm2 , c'est l'une des raisons pour lesquelles cette fonctionnalité mérite d'être construite - nous pouvons donc remplacer les hachages de qualité inférieure dans le cadre.

Grande table de fonctions de hachage avec des caractéristiques de performance et de qualité :
https://github.com/leo-yuriev/t1ha

@arespr Je pense que l'équipe recherche une implémentation C# des fonctions de hachage. Merci pour le partage, cependant.

@tannergooding Vous ne parvenez toujours pas à résoudre ce problème ? Si c'est le cas, je publierai sur Reddit/Twitter que nous recherchons un expert en hachage.

edit: A fait un post sur Reddit. https://www.reddit.com/r/csharp/comments/6qsysm/looking_for_hash_expert_to_help_net_core_team/?ref=share&ref_source=link

@jamesqo , j'ai quelques choses plus prioritaires dans mon assiette et je ne pourrai pas y arriver dans les 3 prochaines semaines.

De plus, les mesures actuelles seront limitées par ce que nous pouvons actuellement coder en C#, cependant, si/quand cela devient une chose (https://github.com/dotnet/designs/issues/13), les mesures changeront probablement quelque peu ;)

De plus, les mesures actuelles seront limitées par ce que nous pouvons actuellement coder en C#, cependant, si/quand cela deviendra une chose (dotnet/designs#13), les mesures changeront probablement quelque peu ;)

C'est OK - nous pouvons toujours changer l'algorithme de hachage une fois que les intrinsèques sont disponibles, encapsuler/randomiser le code de hachage nous permet de le faire. Nous recherchons simplement quelque chose qui offre le meilleur compromis performances/distribution pour le runtime dans son état actuel.

@jamesqo , merci d'avoir cherché des personnes pour vous aider. Nous serions heureux que quelqu'un qui n'est pas un expert en hachage travaille également sur ce sujet. Une fois que nous aurons choisi les candidats, nos experts feront ce que nous faisons en cas de changement : examiner le code pour en vérifier l'exactitude, les performances, la sécurité, etc.

Salut! Je viens de lire la discussion, et au moins pour moi, il semble que l'affaire soit fermement close en faveur du murmur3-32 PoC. Ce qui d'ailleurs me semble être un très bon choix, et je recommanderais de ne plus dépenser de travail inutile (mais peut-être même de laisser tomber les membres .Add() ...).

Mais dans le cas peu probable où quelqu'un voudrait continuer avec plus de travail de performance, je pourrais fournir du code pour xx32, xx64, hsip13/24, seahash, murmur3-x86/32 (et j'ai intégré l'impl marvin32 d'en haut), et (encore non optimisé) sip13/24, spookyv2. Certaines versions de City semblent assez faciles à porter, en cas de besoin. Ce projet à moitié abandonné avait un cas d'utilisation légèrement différent en tête, il n'y a donc pas de classe HashCode avec l'API proposée ; mais pour l'analyse comparative, cela ne devrait pas avoir beaucoup d'importance.

Certainement pas prêt pour la production : le code applique des quantités

Si cela peut vous aider, je devrais trouver suffisamment de temps au cours des deux prochaines semaines pour résoudre les problèmes les plus flagrants et rendre le code et quelques résultats préliminaires disponibles.

@gimpf

Je viens de lire la discussion, et au moins pour moi, il semble que l'affaire soit fermement close en faveur du murmur3-32 PoC. Quel BTW me semble être un très bon choix, et je recommanderais de ne plus dépenser de travail inutile

Non, les gens ne sont pas encore favorables à Murmur3. Nous voulons nous assurer que nous choisissons le meilleur algorithme absolu en termes d'équilibre entre performance/distribution, afin de ne rien négliger.

Mais dans le cas peu probable où quelqu'un voudrait continuer avec plus de travail de performance, je pourrais fournir du code pour xx32, xx64, hsip13/24, seahash, murmur3-x86/32 (et j'ai intégré l'impl marvin32 d'en haut), et (encore non optimisé) sip13/24, spookyv2. Certaines versions de City semblent assez faciles à porter, en cas de besoin.

Oui s'il vous plaît! Nous voulons rassembler du code pour autant d'algorithmes que possible à tester. Chaque nouvel algorithme auquel vous pouvez contribuer est précieux. Ce serait très apprécié si vous pouviez également porter les algorithmes de City.

Certainement pas prêt pour la production : le code applique des quantités généreuses de force brute comme copier-pâtes, étalement cancéreux d'agressif en ligne et dangereux ; l'endianess n'existe pas, pas plus que les lectures non alignées. Même les tests contre les vecteurs de test ref-impl sont par euphémisme "incomplets".

C'est bon. Apportez simplement le code et quelqu'un d'autre peut le trouver si le besoin s'en fait sentir.

Si cela peut vous aider, je devrais trouver suffisamment de temps au cours des deux prochaines semaines pour résoudre les problèmes les plus flagrants et rendre le code et quelques résultats préliminaires disponibles.

Oui ce serait super!

@jamesqo Ok, je laisserai une note une fois que j'aurai quelque chose à montrer.

@gimpf ça sonne vraiment bien et nous aimerions connaître vos progrès au fur et à mesure (pas besoin d'attendre de travailler sur chaque algorithme !). Ne pas être prêt pour la production est acceptable tant que vous pensez que le code produit des résultats corrects et que les performances sont une bonne représentation de ce que nous verrions dans une implémentation prête pour la production. Une fois que nous avons sélectionné les candidats, nous pouvons travailler avec vous pour obtenir des implémentations de haute qualité.

Je n'ai pas vu d'analyse sur la façon dont l'entropie de Seahash se compare à d'autres algorithmes. Avez-vous des indications là-dessus ? Il présente des compromis de performances intéressants... la vectorisation semble rapide, mais l'arithmétique modulaire semble lente.

@morganbr J'ai un teaser prêt.

A propos de SeaHash : Non, je ne connais pas encore la qualité ; au cas où la performance serait intéressante, je l'ajouterais à SMHasher. Au moins, l'auteur prétend que c'est bon (en l'utilisant pour des sommes de contrôle dans un système de fichiers), et prétend également qu'aucune entropie n'est rejetée pendant le mixage.

À propos des hachages et des benchmarks : Project Haschisch.Kastriert , page wiki avec les premiers résultats de benchmarking comparant xx32, xx64, hsip13, hsip24, marvin32, sea et murmur3-32.

Quelques mises en garde importantes :

  • Il s'agissait d'un banc d'essai très rapide avec des paramètres de faible précision.
  • Les implémentations ne sont pas encore vraiment terminées, et certains prétendants manquent encore à l'appel. Les implémentations de Streaming (une telle chose deviendrait nécessaire pour un support .Add() sensible) ont besoin d'une réelle optimisation.
  • SeaHash n'utilise actuellement pas de graine.

Premières impressions:

  • pour les messages volumineux, xx64 est la plus rapide des implémentations répertoriées (environ 3,25 octets par cycle, pour autant que je sache, ou 9,5 Gio/s sur mon ordinateur portable)
  • pour les messages courts, rien n'est génial, mais murmur3-32 et (étonnamment) seahash ont un avantage, mais ce dernier s'explique probablement par seahash n'utilisant pas encore de graine.
  • le "benchmark" pour accéder à un HashSet<> besoin de travail, car tout est presque à l'intérieur de l'erreur de mesure (j'ai vu des différences plus importantes, mais cela ne vaut toujours pas la peine d'en parler)
  • lors de la combinaison de codes de hachage, le PoC murmur-3A est environ 5 à 20 fois plus rapide que ce que nous avons ici
  • certaines abstractions en C# sont très coûteuses ; cela rend la comparaison des algorithmes de hachage plus ennuyeuse que nécessaire.

Je vous écrirai à nouveau une fois que j'aurai un peu amélioré la situation.

@gimpf , c'est un début fantastique ! J'ai regardé le code et les résultats et j'ai quelques questions.

  1. Vos résultats montrent que SimpleMultiplyAdd est environ 5 fois plus lent que Murmur3a de @tannergooding. Cela semble étrange puisque Murmur a plus de travail à faire que multiplier + ajouter (bien que je reconnaisse que la rotation est une opération plus rapide que l'ajout). Est-il possible que vos implémentations aient une inefficacité commune qui ne se trouve pas dans cette implémentation Murmur ou dois-je lire cela comme des implémentations personnalisées ayant un gros avantage par rapport à celles à usage général ?
  2. Avoir des résultats pour 1, 2 et 4 combinaisons est bien, mais cette API va jusqu'à 8. Serait-il possible d'obtenir des résultats pour cela aussi ou cela cause-t-il trop de duplication ?
  3. J'ai vu que vous avez couru sur X64, donc ces résultats devraient nous aider dans le choix de notre algorithme X64, mais d'autres références suggèrent que les algorithmes peuvent différer assez considérablement entre X86 et X64. Est-il facile pour vous d'obtenir également des résultats X86 ? (À un moment donné, nous aurions également besoin d'avoir ARM et ARM64, mais ceux-ci peuvent certainement attendre)

Vos résultats HashSet sont particulièrement intéressants. S'ils tiennent le coup, c'est un cas possible pour préférer une meilleure entropie à un temps de hachage plus rapide.

@morganbr Ce week-end était plus

A propos de vos questions :

  1. Vos résultats montrent que SimpleMultiplyAdd est environ 5 fois plus lent que Murmur3a de @tannergooding. Cela semble étrange...

Je me demandais. C'était une erreur de copier/coller, SimpleMultiplyAdd combinait toujours quatre valeurs... De plus, en réorganisant certaines déclarations, le combineur multiplier-ajouter est devenu légèrement plus rapide (~ 60 % de débit plus élevé).

Est-il possible que vos implémentations aient une inefficacité commune qui ne se trouve pas dans cette implémentation Murmur ou dois-je lire cela comme des implémentations personnalisées ayant un gros avantage par rapport à celles à usage général ?

Certaines choses me manquent probablement, mais il semble que les implémentations à usage général de .NET ne soient pas utilisables pour ce cas d'utilisation. J'ai écrit des méthodes de style Combine pour tous les algorithmes, et la combinaison de code de hachage par rapport à la plupart fonctionne _beaucoup_ mieux que celles à usage général.

Cependant, même ces implémentations restent trop lentes ; des travaux supplémentaires sont nécessaires. Les performances .NET dans ce domaine sont absolument opaques pour moi ; l'ajout ou la suppression d'une copie d'une variable locale peut facilement changer les performances d'un facteur deux. Je ne serai probablement pas en mesure de fournir des implémentations suffisamment bien optimisées dans le but de sélectionner la meilleure option.

  1. Avoir des résultats pour 1, 2 et 4 combinaisons est bien, mais cette API va jusqu'à 8.

J'ai étendu les références de la moissonneuse-batteuse. Pas de surprises de ce côté.

  1. J'ai vu que tu tournais sur X64 (...), est-ce que c'est facile pour toi d'avoir aussi des résultats X86 ?

C'était autrefois, mais j'ai ensuite porté sur .NET Standard. Maintenant, je suis dans l'enfer des dépendances, et seuls les benchmarks .NET Core 2 et CLR 64 bits fonctionnent. Cela peut être résolu assez facilement une fois que j'ai résolu les problèmes actuels.

Pensez-vous que cela le fera dans la version 2.1 ?

@gimpf Vous n'avez pas posté depuis un moment - avez-vous une mise à jour sur l'avancement de vos implémentations ? :souriant:

@jamesqo J'ai corrigé un benchmark qui provoquait des résultats étranges et j'ai ajouté City32, SpookyV2, Sip13 et Sip24 à la liste des algorithmes disponibles. Les Sips sont aussi rapides que prévu (par rapport au débit de xx64), City et Spooky ne le sont pas (c'est toujours le cas pour SeaHash).

Pour combiner des codes de hachage, Murmur3-32 ressemble toujours à un bon pari, mais je n'ai pas encore effectué de comparaison plus exhaustive.

Sur une autre note, l'API de streaming (.Add()) a pour effet secondaire malheureux de supprimer certains algorithmes de hachage de la liste des candidats. Étant donné que les performances d'une telle API sont également discutables, vous voudrez peut-être repenser à l'offrir dès le début.

Si la partie .Add() était évitée, et étant donné que le combinateur de hachage utilise une graine, je ne pense pas qu'il y aurait de mal à nettoyer le combinateur de tg, à créer une petite suite de tests, et appelez ça un jour. Étant donné que je n'ai que quelques heures chaque week-end et que l'optimisation des performances est un peu fastidieuse, la version plaquée or pourrait s'éterniser un peu...

@gimpf , cela semble être un grand progrès. Avez-vous un tableau des résultats à portée de main afin que nous puissions voir s'il y en a assez pour prendre une décision et aller de l'avant ?

@morganbr J'ai mis à jour mes résultats d'analyse comparative .

Pour l'instant, je n'ai que des résultats 64 bits sur .NET Core 2. Pour cette plate-forme, City64 sans graine est le plus rapide dans toutes les tailles. Incorporant une graine, XX-32 est à égalité avec Murmur-3-32. Heureusement, ce sont les mêmes algorithmes qui ont la réputation d'être rapides pour les plates-formes 32 bits, mais nous devons évidemment vérifier que cela est également vrai pour mon implémentation. Les résultats semblent être représentatifs des performances du monde réel, sauf que Sea et SpookyV2 semblent inhabituellement lents.

Vous devrez déterminer dans quelle mesure vous avez réellement besoin d'une protection contre le hachage pour les combinateurs de code de hachage. Si l'ensemencement n'est nécessaire que pour rendre le hachage manifestement inutilisable pour la persistance, city64 une fois XOR avec une graine 32 bits serait une amélioration. Comme cet utilitaire n'est là que pour combiner des hachages (et ne pas remplacer par exemple le code de hachage pour les chaînes, ou être un hachage pour les tableaux d'entiers, etc.), cela pourrait suffire.

Si OTOH vous pensez en avoir besoin, vous serez heureux de voir que Sip13 est généralement moins de 50% plus lent que XX-32 (sur les plates-formes 64 bits), mais ce résultat sera probablement très différent pour les applications 32 bits.

Je ne sais pas à quel point c'est pertinent pour corefx, mais j'ai ajouté les résultats LegacyJit 32bit (w/FW 4.7).

Je voudrais dire que les résultats sont ridiculement lents. Cependant, à titre d'exemple, à 56 MiB/s contre 319 MiB/s, je ne rigole pas (c'est Sip, il manque le plus l'optimisation de rotation à gauche). Je pense que je me souviens pourquoi j'ai annulé mon projet d'algorithme de hachage .NET en janvier...

Ainsi, RyuJit-32bit est toujours manquant et donnera (espérons-le) des résultats très différents, mais pour LegacyJit-x86, Murmur-3-32 gagne facilement, et seuls City-32 et xx-32 peuvent s'en approcher. Murmur a toujours de mauvaises performances à seulement environ 0,4 à 1,1 Go/s au lieu de 0,6 à 2 Go/s (sur la même machine), mais au moins c'est dans la bonne fourchette.

Je vais exécuter les benchmarks sur quelques-unes de mes box ce soir et publier les résultats (Ryzen, i7, Xeon, A10, i7 Mobile et je pense quelques autres).

@tannergooding @morganbr Quelques mises à jour intéressantes et importantes.

Important avant tout :

  • J'ai corrigé certaines implémentations de combinaison qui produisaient des valeurs de hachage incorrectes.
  • La suite de référence travaille désormais plus fort pour éviter un repliement constant. City64 était sensible (comme l'était murmur-3-32 dans le passé). Cela ne veut pas dire que je comprends tous les résultats maintenant, mais ils sont beaucoup plus plausibles.

Bonne choses:

  • Les implémentations du combinateur sont désormais disponibles pour toutes les surcharges d'arguments de 1 à 8, y compris les implémentations un peu plus lourdes à dérouler manuellement pour xx/city.
  • Les tests et les benchmarks les vérifient également. Étant donné que de nombreux algorithmes de hachage ont des messages à faible octet dans des cas particuliers, ces mesures peuvent être intéressantes.
  • Benchmarks d'exécution simplifiés pour plusieurs cibles (Core vs. FW).

Pour exécuter une suite sur toutes les implémentations principales pour combiner les codes de hachage, y compris "Empty" (overhead pur) et "multiply-add" (version optimisée pour la vitesse de la célèbre réponse SO) :

bin\Release\net47\Haschisch.Benchmarks.Net47.exe -j:clr_x86 -j:clr_x64_legacy -j:clr_x64 -j:core_x64 -- CombineHashCode --allcategories=prime

(_L'exécution de benchmarks Core 32 bits semble nécessiter une version préliminaire de BenchmarkDotNet (ou peut-être une configuration 32 bits uniquement plus l'utilisation du Bench-runner basé sur Core). Cela devrait alors fonctionner avec -j:core_x86, espérons-le)_

Résultats : Après toutes les corrections de bugs, xx32 semble l'emporter pour toutes les surcharges avec RyuJIT 64 bits, sous Windows 10 sur un mobile Haswell i7, en un run "rapide". Entre les Sips et marvin32, Sip-1-3 gagne toujours. Sip-1-3 est environ 4 fois plus lent que xx32, qui est à nouveau environ 2 fois plus lent qu'un combineur primitif de multiplication-addition. Les résultats du noyau 32 bits sont toujours manquants, mais j'attends plus ou moins une version stable de BenchmarkDotNet qui résoudra ce problème pour moi.

(Edit) Je viens d'ajouter une exécution rapide d'un benchmark pour accéder à un hash-set . Cela dépend évidemment beaucoup plus des détails que les repères µ ci-dessus, mais vous voudrez peut-être y jeter un coup d'œil.

Merci encore une fois @gimpf pour les données fantastiques ! Voyons si nous pouvons en faire une décision.

Pour commencer, je diviserais les algorithmes comme ceci :
Entropie rapide+bonne (classée par vitesse) :

  1. xxHash32
  2. City64 (cela sera probablement lent sur x86, nous devrons donc probablement choisir autre chose pour x86)
  3. Murmure3A

Résistant au HashDoS :

  • Marvin32
  • SipHash. Si nous penchons vers cela, nous devrons le faire examiner par les experts en cryptographie de Microsoft pour confirmer que les résultats de la recherche sont acceptables. Nous devrons également déterminer quels paramètres sont suffisamment sécurisés. Le document suggère quelque part entre Sip-2-4 et Sip-4-8.

Hors contention (lent) :

  • SpookyV2
  • Ville32
  • xxHash64
    *SeaHash (et nous n'avons pas de données sur l'entropie)

Hors contention (mauvaise entropie) :

  • MultiplierAjouter
  • HSip

Avant de choisir un gagnant, j'aimerais m'assurer que les autres personnes sont d'accord avec mon classement ci-dessus. Si cela tient, je pense que nous devons simplement choisir de payer 2x pour la résistance HashDoS, puis de passer par la vitesse.

@morganbr Votre regroupement semble Jean-Philippe Aumasson , qui a écrit sip-hash avec DJB. Après cette discussion, ils ont décidé d'opter pour sip-1-3 pour les tables de hachage.

(Voir PR rust:#33940 et le problème de rouille qui l'accompagne

Sur la base des données et des commentaires, je voudrais proposer que nous utilisions xxHash32 sur toutes les architectures. La prochaine étape consiste à le mettre en œuvre. @gimpf , êtes-vous intéressé à mettre en place un PR pour cela ?

Pour ceux qui sont préoccupés par HashDoS, je ferai bientôt un suivi avec une proposition d'API de hachage à usage général qui devrait inclure Marvin32 et pourrait inclure SipHash. Ce sera également un endroit approprié pour les autres implémentations sur lesquelles @gimpf et @tannergooding ont travaillé.

@morganbr Je peux mettre en place un PR si le temps le permet. De plus, personnellement, je préférerais aussi xx32, tant qu'il ne réduit pas l'acceptation.

@gimpf , à quoi ressemble ton temps ? Si vous n'avez pas vraiment le temps, nous pouvons également voir si quelqu'un d'autre aimerait tenter le coup.

@morganbr J'avais prévu de le faire jusqu'au 5 novembre, et il semble toujours bon que je trouve le temps dans les deux prochaines semaines.

@gimpf ,

@terrajobst - Je suis un peu en retard à la fête (désolé), mais ne pouvons-nous pas changer le type de retour de la méthode Add ?

```c#
public HashCode Ajouter(valeur T);
public HashCode Ajouter(valeur T, IEqualityComparercomparateur);

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();

Cependant, exactement la même chose peut être réalisée comme ceci, mais avec une allocation de tableau moins inutile :

c# public override int GetHashCode() => new HashCode().Add(Name).Add(Surname).Add(Age).ToHashCode();

Notez que les types peuvent également être mélangés. Cela pourrait évidemment être fait en ne l' appelant pas couramment à l'intérieur d'une méthode régulière. Compte tenu de cet argument selon lequel l'interface fluide n'est pas absolument nécessaire, pourquoi la surcharge inutile de params existe-t-elle pour commencer ? Si cette suggestion est une mauvaise suggestion, alors la surcharge de params tombe sur le même axe. Cela, et forcer une méthode régulière pour un hashcode trivial mais optimal semble être beaucoup de cérémonie.

Edit : Un implicit operator int serait aussi bien pour DRY, mais pas vraiment crucial.

@jcdickinson

ne pouvons-nous pas changer le type de retour de la méthode Add ?

Nous en avons déjà discuté dans l'ancienne proposition, et elle a été rejetée.

pourquoi la surcharge de paramètres inutiles existe-t-elle pour commencer ?

Nous n'ajoutons aucune surcharge de paramètres ? Faites un Ctrl+F pour "params" sur cette page Web, et vous verrez que votre commentaire est le seul endroit où ce mot apparaît.

Un opérateur implicite int serait également bien pour DRY, mais pas exactement crucial.

Je crois que cela a également été discuté quelque part ci-dessus ...

@jamesqo merci pour l'explication.

surcharges de paramètres

Je voulais dire AddRange , mais je suppose qu'il n'y aura pas de traction là-dessus.

@jcdickinson AddRange figurait dans la proposition d'origine, mais ce n'est pas dans la version actuelle. Il a été rejeté par l'examen de l'API (voir https://github.com/dotnet/corefx/issues/14354#issuecomment-308190321 par @terrajobst) :

Nous devrions supprimer toutes les méthodes AddRange car le scénario n'est pas clair. Il est peu probable que les tableaux s'affichent très souvent. Et une fois que de plus grands tableaux sont impliqués, la question est de savoir si le calcul doit être mis en cache. Voir la boucle for du côté appelant montre clairement que vous devez y penser.

@gimpf Je suis allé de l'avant et j'ai la proposition avec xxHash32 . N'hésitez pas à saisir cette implémentation. Il a des tests contre les vecteurs xxHash32 réels.

Éditer

Concernant l'interface. Je suis pleinement conscient que je fais une montagne d'une taupinière - n'hésitez pas à l'ignorer. J'utilise la proposition actuelle contre de vrais trucs et c'est beaucoup de répétitions ennuyeuses.

J'ai joué avec l'interface et je comprends maintenant pourquoi l'interface fluide a été rejetée ; c'est nettement plus lent.

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

Utilisation d'une méthode non intégrée comme source de code de hachage ; 50 invocations de Add vs une méthode d'extension fluide :

| Méthode | Moyenne | Erreur | DevStd | à l'échelle |
|------- |---------:|---------:|---------:|-------: |
| Ajouter | 401,6 secondes | 1,262 ns | 1.180 ns | 1,00 |
| Décompte | 747.8 ns | 2.329 ns | 2,178 ns | 1,86 |

Cependant, le modèle suivant fonctionne :

```c#
structure publique HashCode : System.Collections.IEnumerable
{
[EditorBrowsable(EditorBrowsableState.Never)]
[Obsolète("Cette méthode est fournie pour la syntaxe de l'initialiseur de collection.", erreur : vrai)]
public IEnumerator GetEnumerator() => lancer une nouvelle NotImplementedException();
}

public override int GetHashCode() => new HashCode()
{
    Age, // int
    { Name, StringComparer.Ordinal }, // use Comparer
    Hat // some arbitrary object
}.ToHashCode();

```

Il présente également des caractéristiques de performances identiques à la proposition actuelle :

| Méthode | Moyenne | Erreur | DevStd | à l'échelle |
|------------ |---------:|---------:|---------:|--- ----:|
| Ajouter | 405,0 ns | 2.130 ns | 1,889 ns | 1,00 |
| Initialiseur | 400,8 ns | 4.821 ns | 4,274 ns | 0,99 |

Malheureusement, c'est un peu un hack, car le IEnumerable doit être implémenté pour satisfaire le compilateur. Cela étant dit, le Obsolete produira une erreur même sur foreach - vous devrez vraiment vouloir casser les choses pour rencontrer l'exception. Le MSIL entre les deux est essentiellement identique.

@jcdickinson merci d'avoir saisi le problème. Je vous ai envoyé une invitation Collaborator, faites-moi savoir quand vous acceptez et je pourrai vous attribuer ce problème (en m'attribuant moi-même entre-temps).

Conseil de pro : une fois que vous avez accepté, GitHub vous inscrira automatiquement à toutes les notifications du dépôt (plus de 500 par jour). vous vous êtes abonné.

@jcdickinson , je suis définitivement intéressé par les moyens d'éviter les répétitions ennuyeuses (bien que je

  1. Le problème de perf que vous avez noté
  2. La valeur de retour des méthodes fluides est une copie de la structure. Il est trop facile de perdre accidentellement une entrée en faisant des choses comme :
var hc = new HashCode();
var newHc = hc.Add(foo);
hc.Add(bar);
return newHc.ToHashCode();

Étant donné que la proposition sur ce fil est déjà approuvée (et que vous êtes sur la bonne voie pour la fusionner), je vous suggère de commencer une nouvelle proposition d'API pour tout changement.

@karelz Je crois que @gimpf a déjà saisi ce problème à l'avance. Puisqu'il est plus familiarisé avec l'implémentation, veuillez plutôt attribuer ce problème à @gimpf . ( modifier : nvm)

@terrajobst Une sorte de requête API de dernière minute pour cela. Puisque nous avons marqué GetHashCode obsolète, nous disons implicitement à l'utilisateur que les HashCode ne sont pas des valeurs destinées à être comparées, bien qu'il s'agisse de structures généralement immuables/comparables. Dans ce cas, devrions-nous également marquer Equals obsolète ?

[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.");

Je pense que quelque chose de similaire a été fait avec Span .

Si c'est accepté, alors je pense...

  1. J'envisagerais d'utiliser should not , ou may not au lieu de cannot dans le message Obsolète.
  2. À condition que l'exception reste, je mettrais la même chaîne dans son message, juste au cas où la méthode serait appelée via un cast ou un générique ouvert.

@ Joe4evr Bien avec moi; J'ai mis à jour le commentaire. Il peut également être avantageux d'inclure le même message dans l'exception GetHashCode , puis :

public override int GetHashCode() => throw new NotSupportedException("HashCode is a mutable struct and should not be compared with other HashCodes.");

@morganbr Pourquoi avez-vous rouvert ça ?

Le PR pour l'exposer dans CoreFX n'est pas encore passé.

@gimpf avez-vous le code que vous avez évalué et/ou seriez-vous en mesure de voir rapidement comment le paquet SpookilySharp nuget fonctionne. Je cherche à dépoussiérer ce projet après quelques années de stagnation et je suis curieux de voir comment il tient.

@JonHanna Il l'a posté ici : https://github.com/gimpf/Haschisch.Kastriert

@JonHanna , je serais intéressé de savoir comment se

@morganbr Où serait un forum approprié pour discuter d'une telle API ? Je m'attends à ce qu'une telle API ne se limite pas au plus petit dénominateur commun, et peut-être qu'une bonne API aura également besoin d'un JIT amélioré par rapport à la gestion des structures plus grandes. Discuter de tout ce qui pourrait être mieux fait dans un numéro séparé...

@gimpf En a ouvert un pour vous. dotnet/corefx#25666

@morganbr - Pouvons-nous obtenir le nom et la version du package qui incluront ce commit ?

@karelz , pourriez-vous aider @smitpatel avec les informations sur le package/la version ?

J'essaierais de construire quotidiennement .NET Core - j'attendrais jusqu'à demain.
Je ne pense pas qu'il existe un package sur lequel vous pouvez simplement prendre une dépendance.

Question aux participants ici. L'IDE Roslyn permet aux utilisateurs de générer une impl GetHashCode basée sur un ensemble de champs/propriétés dans leur class/struct . Idéalement, les gens pourraient utiliser le nouveau HashCode.Combine qui a été ajouté dans https://github.com/dotnet/corefx/pull/25013 . Cependant, certains utilisateurs n'auront pas accès à ce code. Nous aimerions donc pouvoir toujours générer un GetHashCode qui fonctionnera pour eux.

Récemment, il est venu à notre attention que le formulaire que nous générons est problématique. À savoir, parce que VB compile avec des contrôles de débordement activés par défaut, et notre impl provoquera des débordements. De plus, VB n'a aucun moyen de désactiver les contrôles de débordement pour une région de code. Il est activé ou désactivé pour l'ensemble de l'assemblage.

Pour cette raison, j'aimerais pouvoir remplacer l'impl que nous fournissons par un formulaire qui ne souffre pas de ces problèmes. Idéalement, le formulaire généré aurait les propriétés suivantes :

  1. Une/deux lignes dans GetHashCode par champ/propriété utilisé.
  2. Pas de débordement.
  3. Hachage raisonnablement bon. Nous ne nous attendons pas à des résultats étonnants. Mais quelque chose qui, espérons-le, a déjà été vérifié pour être décent, et pour ne pas avoir les problèmes que vous rencontrez habituellement avec a + b + c + d ou a ^ b ^ c ^ d .
  4. Aucune dépendance/exigence supplémentaire sur le code.

Par exemple, une option pour VB serait de générer quelque chose comme :

return (a, b, c, d).GetHashCode()

Mais cela dépend alors de la référence à System.ValueTuple. Idéalement, nous pourrions avoir un impl qui fonctionne même en l'absence de cela.

Est-ce que quelqu'un connaît un algorithme de hachage décent qui peut fonctionner avec ces contraintes ? Merci!

--

Remarque : notre code émis existant est :

        Dim hashCode = -252780983
        hashCode = hashCode * -1521134295 + i.GetHashCode()
        hashCode = hashCode * -1521134295 + j.GetHashCode()
        Return hashCode

Cela peut clairement déborder.

Ce n'est pas non plus un problème pour C# car nous pouvons simplement ajouter unchecked { } autour de ce code. Ce contrôle fin n'est pas possible dans VB.

Est-ce que quelqu'un connaît un algorithme de hachage décent qui peut fonctionner avec ces contraintes ? Merci!

Eh bien, vous pourriez faire Tuple.Create(...).GetHashCode() . Évidemment, cela implique des allocations, mais cela semble mieux que de lever une exception.

Y a-t-il une raison pour laquelle vous ne pouvez pas simplement dire à l'utilisateur d'installer System.ValueTuple ? Puisqu'il s'agit d'une fonctionnalité de langage intégrée, je suis sûr que le package System.ValueTuple est très compatible avec pratiquement toutes les plates-formes, n'est-ce pas ?

Évidemment, cela implique des allocations, mais cela semble mieux que de lever une exception.

Oui. ce serait bien que cela ne cause pas d'allocations.

Y a-t-il une raison pour laquelle vous ne pouvez pas simplement dire à l'utilisateur d'installer System.ValueTuple ?

Ce serait le comportement si nous générons l'approche ValueTuple. Cependant, encore une fois, ce serait bien si nous pouvions simplement générer quelque chose de bien qui correspond à la façon dont l'utilisateur a actuellement structuré son code, sans les obliger à modifier leur structure de manière lourde.

Il semble vraiment que les utilisateurs de VB devraient avoir un moyen de résoudre ce problème de manière raisonnable :) Mais une telle approche m'échappe :)

@CyrusNajmabadi , Si vous avez vraiment besoin de faire votre propre calcul de hachage dans le code de l'utilisateur, CRC32 pourrait fonctionner car il s'agit d'une combinaison de recherches de table et de XOR (mais pas d'arithmétique qui peut déborder). Il y a quand même quelques inconvénients :

  1. CRC32 n'a pas une grande entropie (mais c'est probablement encore mieux que ce que Roslyn émet maintenant).
  2. Vous devrez placer une table de recherche de 256 entrées quelque part dans le code ou émettre du code pour générer la table de recherche.

Si vous ne le faites pas déjà, j'espère que vous pourrez détecter le type HashCode et l'utiliser lorsque cela est possible, car XXHash devrait être bien meilleur.

@morganbr Voir https://github.com/dotnet/roslyn/pull/24161

Nous procédons comme suit :

  1. Utilisez System.HashCode s'il est disponible. Terminé.
  2. Sinon, si en C# :
    2a. S'il n'est pas en mode coché : générez un hachage déroulé.
    2b. Si en mode coché : génère un hachage déroulé, enveloppé dans « unchecked{} ».
  3. Sinon, si en VB :
    3b. S'il n'est pas en mode coché : générez un hachage déroulé.
    3c. Si en mode coché, mais a accès à System.ValueTuple : générer Return (a, b, c, ...).GetHashCode()
    3d. Si en mode coché sans accès à System.ValueTuple. Générez un hachage déroulé, mais ajoutez un commentaire dans VB indiquant que les débordements sont très probables.

C'est '3d' c'est vraiment dommage. Fondamentalement, quelqu'un utilisant VB mais n'utilisant pas ValueTuple ou un système récent, ne pourra pas nous utiliser pour obtenir un algorithme de hachage raisonnable généré pour eux.

Vous auriez besoin de mettre une table de recherche de 256 entrées quelque part dans le code

Ce serait complètement désagréable :)

Le code de génération de table est-il également désagréable ? Au moins en suivant l'exemple de Wikipédia , ce n'est pas beaucoup de code (mais il doit quand même aller quelque part dans la source de l'utilisateur).

À quel point serait-il horrible d'ajouter la source HashCode au projet comme le fait Roslyn (avec IL) avec des définitions de classe d'attributs de compilateur (beaucoup plus simples) alors qu'elles ne sont disponibles via aucun assembly référencé ?

À quel point serait-il horrible d'ajouter la source HashCode au projet comme le fait Roslyn avec les définitions de classe d'attributs de compilateur (beaucoup plus simples) alors qu'elles ne sont disponibles via aucun assembly référencé ?

  1. La source HashCode n'a-t-elle pas besoin d'un comportement de débordement ?
  2. J'ai parcouru la source HashCode. Ce n'est pas trivial. Générer tout ce goop dans le projet de l'utilisateur serait assez lourd.

Je suis juste surpris qu'il n'y ait aucun bon moyen de faire fonctionner les mathématiques de débordement dans VB :(

Donc, au minimum, même si nous hachions deux valeurs ensemble, il semble que nous devions créer :

```c#
var hc1 = (uint)(value1?.GetHashCode() ?? 0); // peut déborder
var hc2 = (uint)(value2?.GetHashCode() ?? 0); // peut déborder

        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;
        }

tout comme QueueRound :

c# private static uint QueueRound(uint hash, uint queuedValue) { hash += queuedValue * Prime3; return Rol(hash, 17) * Prime4; }

Donc honnêtement je ne vois pas comment cela fonctionnerait :(

Comme ce serait horrible d'ajouter la source HashCode au projet comme le fait Roslyn (avec IL) avec (le plus

Comment envisagez-vous ce fonctionnement ? Qu'écriraient les clients et que feraient les compilateurs en réponse ?

De plus, quelque chose qui résoudrait tout cela est si .Net a déjà des aides publiques exposées sur l'API de surface qui convertissent de uint en int32 (et vice versa) sans débordement.

Ceux-ci existent-ils ? Si c'est le cas, je peux facilement écrire les versions VB, en les utilisant uniquement pour les situations où nous devons passer d'un type à l'autre sans déborder.

Le code de génération de table est-il également désagréable ?

Je le penserais. Je veux dire, pensez à cela du point de vue du client. Ils veulent juste une méthode GetHashCode décente qui soit bien autonome et donne des résultats raisonnables. Avoir cette fonctionnalité aller gonfler leur code avec de la merde auxiliaire va être assez désagréable. C'est également assez mauvais étant donné que l'expérience C# sera très bien.

Vous pourrez peut-être obtenir à peu près le bon comportement de débordement en transtypant vers et depuis une combinaison de types 64 bits signés et non signés. Quelque chose comme ça (non testé et je ne connais pas la syntaxe de coulée VB):

Dim hashCode = -252780983
hashCode = (Int32)((Int32)((Unt64)hashCode * -1521134295) + (UInt64)i.GetHashCode())

Comment savez-vous que ce qui suit ne déborde pas ?

c# (Int32)((Unt64)hashCode * -1521134295)

Ou le casting final (int32) d'ailleurs ?

Je ne savais pas qu'il utiliserait des opérations de conv vérifiées par débordement. Je suppose que vous pouvez le masquer à 32 bits avant de lancer :

(Int32)(((Unt64)hashCode * -1521134295) & 0xFFFFFFFF)

vraisemblablement 31 bits, car une valeur de uint32.Max déborderait également lors de la conversion en Int32 :)

C'est def possible. Moche... mais possible :) Il y aura beaucoup de castes dans ce code.

D'accord. Je pense avoir une solution viable. Le cœur de l'algorithme que nous générons aujourd'hui est :

c# hashCode = hashCode * -1521134295 + j.GetHashCode();

Disons que nous faisons des maths 64 bits, mais "hashCode" a été limité à 32 bits. Alors <largest_32_bit> * -1521134295 + <largest_32_bit> ne débordera pas de 64 bits. Nous pouvons donc toujours faire le calcul en 64 bits, puis réduire à 32 (ou 32 bits) pour s'assurer que le prochain tour ne débordera pas.

Merci!

@MaStr11 @morganbr @sharwell et tout le monde ici. J'ai mis à jour mon code pour générer ce qui suit pour VB :

        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)

Quelqu'un peut-il me contrôler pour s'assurer que cela a du sens et ne devrait pas déborder même avec le mode coché activé?

@CyrusNajmabadi , cela ne débordera pas (car Int64.Max = Int32.Max*Int32.Max et vos constantes sont beaucoup plus petites que cela) mais vous masquez le bit haut à zéro, ce n'est donc qu'un hachage de 31 bits. Le fait de laisser le bit haut activé est-il considéré comme un débordement ?

@CyrusNajmabadi hashCode est un Long qui peut aller de 0 à Integer.MaxValue . Pourquoi est-ce que je reçois ça ?

image

Mais non, il ne peut pas vraiment déborder.

Au fait, je préférerais que Roslyn ajoute un package NuGet plutôt que d'ajouter un hachage sous-optimal.

mais vous masquez le bit de poids fort à zéro, ce n'est donc qu'un hachage de 31 bits. Le fait de laisser le bit haut activé est-il considéré comme un débordement ?

C'est un bon point. Je pense que je pensais à un autre algorithme qui utilisait des uints. Donc, afin de convertir en toute sécurité du long en uint, je devais ne pas inclure le bit de signe. Cependant, comme il s'agit de mathématiques signées, je pense qu'il serait bon de masquer simplement 0xffffffff en veillant à ne conserver que les 32 bits inférieurs après avoir ajouté chaque entrée.

Je préférerais que Roslyn ajoute un package NuGet plutôt que d'ajouter un hachage sous-optimal.

Les utilisateurs peuvent déjà le faire s'ils le souhaitent. Il s'agit de ce qu'il faut faire lorsque les utilisateurs n'ajoutent pas ou ne peuvent pas ajouter ces dépendances. Il s'agit également de fournir un hachage raisonnablement « assez bon » pour les utilisateurs. c'est-à-dire quelque chose de mieux que l'approche commune "x + y + z" que les gens adoptent souvent. Il n'est pas destiné à être « optimal » car il n'y a pas de bonne définition de ce qu'est « optimal » en matière de hachage pour tous les utilisateurs. Notez que l'approche que nous adoptons ici est celle déjà émise par le compilateur pour les types anonymes. Il présente un comportement raisonnablement bon sans ajouter une tonne de complexité au code de l'utilisateur. Au fur et à mesure que de plus en plus d'utilisateurs sont capables d'aller de l'avant, cela peut lentement disparaître et être remplacé par HashCode.Combine pour la plupart des gens.

J'ai donc travaillé un peu et j'ai trouvé ce qui suit qui, je pense, répond à toutes les préoccupations :

        Dim hashCode As Long = 2118541809
        hashCode = (hashCode * -1521134295 + a.GetHashCode()).GetHashCode()
        hashCode = (hashCode * -1521134295 + b.GetHashCode()).GetHashCode()
        Return CType(hashCode, Integer)

La pièce qui est intéressante appelle spécifiquement .GetHashCode() sur la valeur int64 produite par (hashCode * -1521134295 + a.GetHashCode()) . Appeler .GetHashCode sur cette valeur 64 bits a deux bonnes propriétés pour nos besoins. Premièrement, il garantit que hashCode n'y stocke qu'une valeur int32 légale (ce qui rend le cast de retour final toujours sûr à exécuter). Deuxièmement, cela garantit que nous ne perdons aucune information précieuse dans les 32 bits supérieurs de la valeur temp int64 avec laquelle nous travaillons.

@CyrusNajmabadi Proposer en fait d'installer le package est ce que je demandais. Cela m'évite d'avoir à le faire.

Si vous tapez HashCode, alors si System.HashCode est fourni dans un package MS nuget, Roslyn le proposera.

Je veux qu'il génère la surcharge GetHashCode inexistante et installe le package dans la même opération.

Je ne pense pas que ce soit un choix approprié pour la plupart des utilisateurs. L'ajout de dépendances est une opération très lourde à laquelle les utilisateurs ne devraient pas être contraints. Les utilisateurs peuvent décider du bon moment pour faire ces choix, et l'IDE le respectera. C'est l'approche que nous avons adoptée avec toutes nos fonctionnalités jusqu'à présent, et c'est une approche saine que les gens semblent aimer.

Remarque : dans quel package nuget cette API est-elle incluse pour que nous puissions ajouter une référence ?

L'implémentation est dans System.Private.CoreLib.dll, elle ferait donc partie du package d'exécution. Le contrat est System.Runtime.dll.

D'accord. Si tel est le cas, il semble qu'un utilisateur l'obtiendrait s'il passe à un cadre cible plus récent. Ce genre de chose n'est pas du tout une étape que j'aurais le "générer égal + code de hachage" faire au projet d'un utilisateur.

Cette page vous a été utile?
0 / 5 - 0 notes