Runtime: Propuesta: Agregar System.HashCode para facilitar la generación de buenos códigos hash.

Creado en 9 dic. 2016  ·  182Comentarios  ·  Fuente: dotnet/runtime

Actualización 16/6/17: Buscando voluntarios

Se finalizó la forma de la API. Sin embargo, todavía estamos decidiendo cuál es el mejor algoritmo hash de una lista de candidatos para usar en la implementación, y necesitamos a alguien que nos ayude a medir el rendimiento / distribución de cada algoritmo. Si desea asumir ese rol, deje un comentario a continuación y @karelz le asignará este problema.

Actualización 13/06/17: ¡propuesta aceptada!

Aquí está la API que fue aprobada por @terrajobst en 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();
    }
}

A continuación se presenta el texto original de esta propuesta.

Razón fundamental

La generación de un buen código hash no debería requerir el uso de constantes mágicas desagradables y cambios de bits en nuestro código. Debería ser menos tentador escribir una implementación GetHashCode mala pero concisa como

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

Propuesta

Deberíamos agregar un tipo HashCode para encapsular la creación de código hash y evitar que los desarrolladores se mezclen en los detalles confusos. Aquí está mi propuesta, que se basa en https://github.com/dotnet/corefx/issues/14354#issuecomment -305019329, con algunas modificaciones menores.

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

Observaciones

Ver @terrajobst comentario 's en https://github.com/dotnet/corefx/issues/14354#issuecomment -305019329 para los objetivos de esta API; todas sus observaciones son válidas. Sin embargo, me gustaría señalar estos en particular:

  • La API no necesita producir un hash criptográfico fuerte
  • La API proporcionará "un" código hash, pero no garantizará un algoritmo de código hash en particular. Esto nos permite usar un algoritmo diferente más tarde o usar algoritmos diferentes en arquitecturas diferentes.
  • La API garantizará que dentro de un proceso dado, los mismos valores producirán el mismo código hash. Es probable que diferentes instancias de la misma aplicación produzcan diferentes códigos hash debido a la aleatorización. Esto nos permite asegurarnos de que los consumidores no puedan conservar los valores hash y depender accidentalmente de que sean estables en todas las ejecuciones (o peor aún, en las versiones de la plataforma).
api-approved area-System.Numerics up-for-grabs

Comentario más útil

Decisiones

  • Deberíamos eliminar todos los métodos AddRange porque el escenario no está claro. Es poco probable que las matrices aparezcan con mucha frecuencia. Y una vez que se involucran matrices más grandes, la pregunta es si el cálculo debe almacenarse en caché. Ver el bucle for en el lado de la llamada deja en claro que debes pensar en eso.
  • Tampoco queremos agregar IEnumerable sobrecargas a AddRange porque se asignarían.
  • No creemos que necesitemos la sobrecarga de Add que requiere string y StringComparison . Sí, es probable que sean más eficientes que llamar a través del IEqualityComparer , pero podemos solucionarlo más tarde.
  • Creemos que marcar GetHashCode como obsoltete con error es una buena idea, pero iríamos un paso más allá y también nos esconderíamos de IntelliSense.

Esto nos deja con:

`` C #
// Vivirá en la asamblea central
// .NET Framework: mscorlib
// .NET Core: System.Runtime / System.Private.CoreLib
sistema de espacio de nombres
{
estructura pública HashCode
{
public static int Combine(Valor T11);
public static int Combine(Valor T1 1, valor T2 2);
public static int Combine(T1 valor1, T2 valor2, T3 valor3);
public static int Combine(T1 valor1, T2 valor2, T3 valor3, T4 valor4);
public static int Combine(T1 valor1, T2 valor2, T3 valor3, T4 valor4, T5 valor5);
public static int Combine(T1 valor1, T2 valor2, T3 valor3, T4 valor4, T5 valor5, T6 valor6);
public static int Combine(T1 valor1, T2 valor2, T3 valor3, T4 valor4, T5 valor5, T6 valor6, T7 valor7);
public static int Combine(T1 valor1, T2 valor2, T3 valor3, T4 valor4, T5 valor5, T6 valor6, T7 valor7, T8 valor8);

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

}
''

Todos 182 comentarios

Propuesta: agregar soporte de aleatorización de hash

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

Se necesita T o Type type para obtener el mismo hash aleatorio para el mismo tipo.

Propuesta: agregar soporte para colecciones

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

Creo que no hay necesidad de sobrecargas Combine(_field1, _field2, _field3, _field4, _field5) porque el siguiente código HashCode.Empty.Combine(_field1).Combine(_field2).Combine(_field3).Combine(_field4).Combine(_field5); debería estar optimizado en línea sin llamadas Combine.

@AlexRadch

Propuesta: agregar soporte para colecciones

Sí, eso fue parte de mi plan eventual para esta propuesta. Sin embargo, creo que es importante centrarse en cómo queremos que se vea la API antes de comenzar a agregar esos métodos.

Quería usar un algoritmo diferente, como el hash Marvin32 que se usa para cadenas en coreclr. Esto requeriría expandir el tamaño de HashCode a 8 bytes.

¿Qué hay de tener tipos Hash32 y Hash64 que almacenarían internamente 4 u 8 bytes de datos? Documente los pros y los contras de cada uno. Hash64 es bueno para X, pero potencialmente más lento. Hash32 es más rápido, pero potencialmente no tan distribuido (o cualquiera que sea la compensación).

Quería aleatorizar la semilla de hachís, por lo que los valores hash no serían deterministas.

Este parece un comportamiento útil. Pero pude ver personas que querían controlar esto. Entonces, quizás debería haber dos formas de crear el Hash, una que no tome semilla (y use una semilla aleatoria) y otra que permita que se proporcione la semilla.

Nota: A Roslyn le encantaría si esto se pudiera proporcionar en el Fx. Estamos agregando una función para escupir un GetHashCode para el usuario. Actualmente, genera código como:

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

Esta no es una gran experiencia y expone muchos conceptos desagradables. Estaríamos encantados de tener un Hash, cualquier API a la que podamos llamar en su lugar.

¡Gracias!

¿Qué pasa con MurmurHash? Es razonablemente rápido y tiene muy buenas propiedades hash. También hay dos implementaciones diferentes, una que escupe hashes de 32 bits y otra que escupe hashes de 128 bits.

También hay implementaciones vectorizadas para los formatos de 32 y 128 bits.

@tannergooding MurmurHash es rápido, pero no seguro, según los sonidos de esta publicación de

@jkotas , ¿ha habido algún trabajo en el JIT en torno a la generación de un mejor código para estructuras de> 4 bytes en 32 bits desde nuestras discusiones el año pasado? Además, ¿qué opinas de la propuesta de @CyrusNajmabadi ?

¿Qué hay de tener tipos Hash32 y Hash64 que almacenarían internamente 4 u 8 bytes de datos? Documente los pros y los contras de cada uno. Hash64 es bueno para X, pero potencialmente más lento. Hash32 es más rápido, pero potencialmente no tan distribuido (o cualquiera que sea la compensación).

Sigo pensando que este tipo sería muy valioso para ofrecerlo a los desarrolladores y sería genial tenerlo en 2.0.

@jamesqo , no creo que esta implementación deba ser criptográficamente segura (ese es el propósito de las funciones explícitas de hash criptográfico).

Además, ese artículo se aplica a Murmur2. El problema se ha resuelto en el algoritmo Murmur3.

el JIT en torno a la generación de un mejor código para estructuras de> 4 bytes en 32 bits desde nuestras discusiones el año pasado

No estoy enterada de nada.

¿Qué opinas de la propuesta de @CyrusNajmabadi ?

Los tipos de marcos deben ser opciones simples que funcionen bien para el 95% o más de los casos. Puede que no sean los más rápidos, pero está bien. Tener que elegir entre Hash32 y Hash64 no es una elección sencilla.

Eso está bien para mí. Pero, ¿podemos al menos tener una solución lo suficientemente buena para esos casos del 95%? Ahora mismo no hay nada ...: - /

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

@CyrusNajmabadi ¿Por qué llamas a EqualityComparer aquí, y no solo a this.s.GetHashCode ()?

Para no estructuras: para que no necesitemos verificar si hay nulos.

Esto también se acerca a lo que generamos para tipos anónimos detrás de escena. Optimizo el caso de valores conocidos no nulos para generar código que sería más agradable para los usuarios. Pero sería bueno tener una API incorporada para esto.

La llamada a EqualityComparer.Default.GetHashCode es como 10 veces más cara que la comprobación de nulos ....

La llamada a EqualityComparer.Default.GetHashCode es como 10 veces más cara que la comprobación de valores nulos.

Suena como un problema. si solo hubiera una buena API de código hash, podríamos llamar al Fx al que podría aplazar :)

(Además, tenemos ese problema en nuestros tipos anónimos, ya que eso es lo que generamos allí también).

No estoy seguro de lo que hacemos con las tuplas, pero supongo que es similar.

No estoy seguro de lo que hacemos con las tuplas, pero supongo que es similar.

System.Tuple pasa por EqualityComparer<Object>.Default por razones históricas. System.ValueTuple llama a Object.GetHashCode con verificación nula - https://github.com/dotnet/coreclr/blob/master/src/mscorlib/shared/System/ValueTuple.cs#L809.

Oh no. Parece que tuple solo puede usar "HashHelpers". ¿Podría exponerse eso para que los usuarios puedan obtener el mismo beneficio?

Excelente. Estoy feliz de hacer algo similar. Empecé con nuestros tipos anónimos porque pensé que eran las mejores prácticas razonables. Si no, está bien. :)

Pero no es por eso que estoy aquí. Estoy aquí para conseguir un sistema que realmente combine los hashes de forma eficaz. Si / cuando eso se pueda proporcionar, con gusto pasaremos a llamar a eso en lugar de codificar en números aleatorios y combinar valores hash nosotros mismos.

¿Cuál sería la forma de API que cree que funcionaría mejor para el código generado por el compilador?

Literalmente, cualquiera de las soluciones de 32 bits que se presentaron anteriormente estaría bien para mí. Diablos, las soluciones de 64 bits están bien para mí. Solo una especie de API que puede obtener que dice "puedo combinar hashes de alguna manera razonable y producir un resultado distribuido razonablemente".

No puedo conciliar estas declaraciones:

Teníamos una estructura HashCode inmutable que tenía un tamaño de 4 bytes. Tenía un método Combine (int), que mezclaba el código hash proporcionado con su propio código hash a través de un algoritmo similar a DJBX33X y devolvía un nuevo HashCode.

@jkotas no pensó que el algoritmo similar a DJBX33X fuera lo suficientemente robusto.

Y

Los tipos de marcos deben ser opciones simples que funcionen bien para el 95% o más de los casos.

¿No podemos encontrar un hash acumulativo simple de 32 bits que funcione lo suficientemente bien para el 95% de los casos? ¿Cuáles son los casos que no se manejan bien aquí y por qué creemos que están en el caso del 95%?

@jkotas , ¿el rendimiento es realmente tan crítico para este tipo? Creo que, en promedio, cosas como búsquedas de tablas hash y esto tomaría mucho más tiempo que unas pocas copias de estructura. Si resulta ser un cuello de botella, ¿sería razonable pedirle al equipo de JIT que optimice las copias de estructura de 32 bits después del lanzamiento de la API para que tengan algún incentivo, en lugar de bloquear esta API cuando nadie está trabajando en la optimización? copias?

¿No podemos encontrar un hash acumulativo simple de 32 bits que funcione lo suficientemente bien para el 95% de los casos?

Nos han quemado mucho por defecto acumulando hash de 32 bits para cadenas, y es por eso que Marvin hash para cadenas en .NET Core: https://github.com/dotnet/corert/blob/87e58839d6629b5f90777f886a2f52d7a99c076f/src/System.Private.CoreLib/ src / System / Marvin.cs # L25. No creo que queramos repetir el mismo error aquí.

@jkotas , ¿el rendimiento es realmente tan crítico para este tipo?

No creo que la actuación sea crítica. Dado que parece que esta API será utilizada por el código del compilador generado automáticamente, creo que deberíamos preferir un código generado más pequeño sobre su apariencia. El patrón no fluido es un código más pequeño.

Hemos sido quemados muy mal por defecto acumulando hash de 32 bits para cadena

Ese no parece ser el caso del 95%. Estamos hablando de desarrolladores normales que solo quieren un hash "suficientemente bueno" para todos aquellos tipos en los que hoy hacen cosas manualmente.

Dado que parece que esta API será utilizada por el código del compilador generado automáticamente, creo que deberíamos preferir un código generado más pequeño sobre su apariencia. El patrón no fluido es un código más pequeño.

Esto no es para uso del compilador Roslyn. Esto es para que lo use Roslyn IDE cuando ayudamos a los usuarios a generar GetHashCodes para sus tipos. Este es un código que el usuario verá y tendrá que mantener, y que tiene algo sensato como:

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

Quiero decir, ya tenemos este código en Fx:

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

Creemos que es lo suficientemente bueno para tuplas. No me queda claro por qué sería tan problemático ponerlo a disposición de los usuarios que lo deseen para sus propios tipos.

Nota: incluso hemos considerado hacer esto en roslyn:

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

Pero ahora está obligando a las personas a generar una estructura (potencialmente grande) solo para obtener algún tipo de comportamiento de hash predeterminado razonable.

Estamos hablando de desarrolladores normales que solo quieren un hash "suficientemente bueno" para todos aquellos tipos en los que hoy hacen cosas manualmente.

El hash de cadena original era un hash "suficientemente bueno" que funcionaba bien para los desarrolladores normales. Pero luego se descubrió que los servidores web ASP.NET eran vulnerables a los ataques DoS porque tienden a almacenar las cosas recibidas en tablas hash. Entonces, el hash "suficientemente bueno" básicamente se convirtió en un problema de seguridad malo.

Creemos que es lo suficientemente bueno para tuplas.

No necesariamente. Hicimos una medida de back stop para tuplas para hacer que el código hash sea aleatorio, lo que nos da la opción de modificar el algoritmo más tarde.

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

Esto me parece razonable.

No entiendo tu posición. Parece que estás diciendo dos cosas:

El hash de cadena original era un hash "suficientemente bueno" que funcionaba bien para los desarrolladores normales. Pero luego se descubrió que los servidores web ASP.NET eran vulnerables a los ataques DoS porque tienden a almacenar las cosas recibidas en tablas hash. Entonces, el hash "suficientemente bueno" básicamente se convirtió en un problema de seguridad malo.

De acuerdo, si ese es el caso, proporcionemos un código hash que sea bueno para las personas que tienen problemas de seguridad / DoS.

Los tipos de marcos deben ser opciones simples que funcionen bien para el 95% o más de los casos.

Bien, si ese es el caso, proporcionemos un código hash que sea lo suficientemente bueno para el 95% de los casos. Las personas que tengan problemas de seguridad / DoS pueden usar los formularios especializados que están documentados para ese propósito.

No necesariamente. Hicimos una medida de back stop para tuplas para hacer que el código hash sea aleatorio, lo que nos da la opción de modificar el algoritmo más tarde.

Está bien. ¿Podemos exponer eso para que los usuarios puedan usar ese mismo mecanismo?

-
Realmente estoy luchando aquí porque parece que estamos diciendo "porque no podemos hacer una solución universal, todos tienen que desarrollar la suya". Ese parece ser uno de los peores lugares para estar. Porque ciertamente la mayoría de nuestros clientes no están pensando en lanzar su propio 'hash marvin' para las preocupaciones de DoS. Solo están agregando, haciendo xoring o combinando de manera deficiente los hashes de campo en un hash final.

Si nos preocupamos por el caso del 95%, entonces deberíamos hacer un hash enogh generalmente bueno. SI nos preocupamos por el caso del 5%, podemos proporcionar una solución especializada para eso.

Esto me parece razonable.

Genial :) ¿Podemos entonces exponer:

`` c #
espacio de nombres System.Numerics.Hashing
{
clase estática interna HashHelpers
{
public static readonly int RandomSeed = new Random (). Siguiente (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);

Esto tendría el beneficio de ser realmente "suficientemente bueno" para la gran mayoría de los casos, al mismo tiempo que guiaría a las personas por el buen camino de inicializar con valores aleatorios para que no dependan de hashes no aleatorios.

Las personas que tengan problemas de seguridad / DoS pueden usar los formularios especializados que están documentados para ese propósito.

Cada aplicación ASP.NET tiene un problema de seguridad / DoS.

Genial :) ¿Podemos entonces exponer:

Esto es diferente de lo que he dicho que es razonable.

¿Qué opinas sobre https://github.com/aspnet/Common/blob/dev/shared/Microsoft.Extensions.HashCodeCombiner.Sources/HashCodeCombiner.cs ? Es lo que se usa en ASP.NET internamente en varios lugares hoy en día, y es con lo que estaría bastante contento (excepto que la función de combinación debe ser más sólida: detalles de implementación que podemos seguir modificando).

@jkotas Escuché que: p

Entonces, el problema aquí es que los desarrolladores no saben cuándo son susceptibles a los ataques DoS, porque no es algo que les importe, por lo que cambiamos las cadenas para usar Marvin32.

No debemos tomar la ruta de decir "el 95% de los casos no importan", porque no tenemos forma de demostrarlo, y debemos pecar de cautelosos incluso cuando tiene un costo de rendimiento. Si va a alejarse de eso, entonces la implementación del código hash necesita una revisión de Crypto Board, no solo que nosotros decidamos "Esto se ve lo suficientemente bien".

Cada aplicación ASP.NET tiene un problema de seguridad / DoS.

Está bien. Entonces, ¿cómo está lidiando con el problema hoy en día de que nadie tiene ayuda con los códigos hash y, por lo tanto, es probable que esté haciendo las cosas mal? Claramente ha sido aceptable tener ese estado del mundo. Entonces, ¿qué se perjudica al proporcionar un sistema de hash razonable que probablemente funcione mejor que el que la gente está manejando hoy?

porque no tenemos forma de demostrarlo, y debemos pecar de cautelosos incluso cuando tiene un costo de rendimiento

Si no proporciona algo, la gente seguirá haciendo las cosas mal. El rechazo de lo "suficientemente bueno" porque no hay nada perfecto solo significa el pobre status quo que tenemos hoy.

Cada aplicación ASP.NET tiene un problema de seguridad / DoS.

¿Puede explicar esto? Según tengo entendido, tiene un problema de DoS si acepta entradas arbitrarias y luego las almacena en alguna estructura de datos que funciona mal si las entradas se pueden diseñar especialmente. Ok, entiendo cómo eso es una preocupación con las cadenas que uno obtiene en escenarios web que provienen del usuario.

Entonces, ¿cómo se aplica eso al resto de tipos que no se están utilizando en este escenario?

Tenemos estos conjuntos de tipos:

  1. Tipos de usuarios que deben ser seguros DoS. En este momento no proporcionamos nada para ayudar, por lo que ya estamos en un mal lugar, ya que es probable que la gente no esté haciendo lo correcto.
  2. Tipos de usuarios que no necesitan ser seguros DoS. En este momento no proporcionamos nada para ayudar, por lo que ya estamos en un mal lugar, ya que es probable que la gente no esté haciendo lo correcto.
  3. Tipos de marcos que deben ser seguros para DoS. En este momento los hemos hecho seguros DoS, pero a través de las API no los exponemos.
  4. Tyeps de framework que no necesitan ser seguros para DoS. Ahora mismo les hemos dado hashes, pero a través de API no exponemos.

Básicamente, creemos que estos casos son importantes, pero no lo suficientemente importantes como para proporcionar una solución a los usuarios para manejar '1' o '2'. Debido a que nos preocupa que una solución para '2' no sea buena para '1', ni siquiera la proporcionaremos en primer lugar. Y si ni siquiera estamos dispuestos a proporcionar una solución para '1', se siente como si estuviéramos en una posición increíblemente extraña. Estamos preocupados por DoSing y ASP, pero no nos preocupamos demasiado por ayudar a la gente. Y debido a que no ayudaremos a las personas con eso, ni siquiera estamos dispuestos a ayudar en los casos que no son DoS.

-

Si estos dos casos son importantes (que estoy dispuesto a aceptar), ¿por qué no dar dos API? Documentarlos. Déjeles en claro para qué sirven. Si la gente los usa correctamente, genial . Si la gente no los usa correctamente, está bien. Después de todo, es probable que no estén haciendo las cosas correctamente hoy de todos modos , entonces, ¿cómo están las cosas peor?

Qué piensa usted acerca de

No tengo ninguna opinión de una forma u otra. Si se trata de una API que los clientes pueden usar y que funciona de manera aceptable y que proporciona una API simple con un código claro en su extremo, entonces creo que está bien.

Creo que sería bueno tener un formulario estático simple que maneje el caso del 99% de querer combinar un conjunto de campos / propiedades de manera ordenada. Parece que algo así podría agregarse a este tipo de manera bastante simple.

Creo que sería bueno tener una forma estática simple

De acuerdo.

Creo que sería bueno tener un formulario estático simple que maneje el caso del 99% de querer combinar un conjunto de campos / propiedades de manera ordenada. Parece que algo así podría agregarse a este tipo de manera bastante simple.

De acuerdo.

Estoy dispuesto a encontrarme con ustedes a la mitad en este caso porque realmente quiero ver algún tipo de API. @jkotas Todavía no entiendo que se h.Combine(a).Combine(b) (versión inmutable) es más corta que h.Combine(a); h.Combine(b); (mutable versión)).

Dicho esto, estoy dispuesto a volver a:

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
}

¿Parece esto razonable?

No puedo editar mi publicación en este momento, pero me di cuenta de que no todos los métodos pueden aceptar T. En ese caso, podemos tener solo 8 sobrecargas que acepten todas las entradas y forzar al usuario a llamar a GetHashCode.

Si estos dos casos son importantes (que estoy dispuesto a aceptar), ¿por qué no dar dos API? Documentarlos. Déjeles en claro para qué sirven. Si la gente los usa correctamente, genial. Si la gente no los usa correctamente, está bien. Después de todo, es probable que no estén haciendo las cosas correctamente hoy de todos modos, entonces, ¿cómo están las cosas peor?

Porque la gente no usa las cosas correctamente cuando están ahí. Tomemos un ejemplo simple, XSS. Desde el principio, incluso los formularios web tenían la capacidad de codificar la salida en HTML. Sin embargo, los desarrolladores no conocían el riesgo, no sabían cómo hacerlo correctamente y solo se enteraron cuando ya era demasiado tarde, su aplicación se publicó y, vaya, ahora su cookie de autenticación se eliminó.

Dar a las personas una opción de seguridad supone que

  1. Conozca el problema.
  2. Comprenda cuáles son los riesgos.
  3. Puede evaluar esos riesgos.
  4. Puede descubrir fácilmente lo que debe hacer.

Esas suposiciones generalmente no son válidas para la mayoría de los desarrolladores, solo se enteran del problema cuando es demasiado tarde. Los desarrolladores no asisten a conferencias de seguridad, no leen informes técnicos y no comprenden las soluciones. Entonces, en el escenario ASP.NET HashDoS, tomamos la decisión por ellos, los protegemos de manera predeterminada, porque eso era lo correcto y tenía el mayor impacto. Sin embargo, solo lo aplicamos a las cadenas, y eso dejó a las personas que estaban construyendo clases personalizadas a partir de la entrada del usuario en un mal lugar. Debemos hacer lo correcto y ayudar a proteger a esos clientes ahora, y convertirlo en la opción predeterminada, tener un pozo de éxito, no de fracaso. El diseño de API para la seguridad a veces no se trata de opciones, sino de ayudar al usuario, lo sepa o no.

Un usuario siempre puede crear un hash no centrado en la seguridad; así que dadas las dos opciones

  1. La utilidad hash predeterminada no tiene en cuenta la seguridad; el usuario puede crear una función hash consciente de la seguridad
  2. La utilidad hash predeterminada tiene en cuenta la seguridad; el usuario puede crear una función hash personalizada que no tenga en cuenta la seguridad

Entonces el segundo probablemente sea mejor; y lo que se sugiere no tendría el impacto de rendimiento de un hash criptográfico completo; ¿Entonces hace un buen compromiso?

Una de las preguntas frecuentes en estos hilos ha sido qué algoritmo es perfecto para todos. Creo que es seguro decir que no hay un solo algoritmo perfecto. Sin embargo, no creo que eso deba impedirnos proporcionar algo mejor que un código como el que ha mostrado @CyrusNajmabadi , que tiende a tener una entropía pobre para las entradas de .NET comunes, así como otros errores comunes de hasher (como perder datos de entrada o ser fácilmente reiniciable).

Me gustaría proponer un par de opciones para solucionar el problema del "mejor algoritmo":

  1. Opciones explícitas: planeo enviar una propuesta de API pronto para un conjunto de hashes no criptográficos (tal vez xxHash, Marvin32 y SpookyHash, por ejemplo). Dicha API tiene un uso ligeramente diferente al de un tipo HashCode o HashCodeHelper, pero por el bien de la discusión, supongamos que podemos resolver esas diferencias. Si usamos esa API para GetHashCode:

    • El código generado es explícito sobre lo que está haciendo: si Roslyn genera Marvin32.Create(); , les permite a los usuarios avanzados saber lo que decidieron hacer y pueden cambiarlo fácilmente a otro algoritmo en la suite si así lo desean.

    • Significa que no tenemos que preocuparnos por cambios importantes. Si comenzamos con un algoritmo lento / no aleatorizado / de entropía pobre, podemos simplemente actualizar Roslyn para comenzar a generar algo más en código nuevo. El código antiguo seguirá usando el hash anterior y el código nuevo usará el hash nuevo. Los desarrolladores (o una corrección de código de Roslyn) pueden cambiar el código antiguo si así lo desean.

    • El mayor inconveniente en el que puedo pensar es que algunas de las optimizaciones que podríamos desear para GetHashCode podrían ser perjudiciales para otros algoritmos. Por ejemplo, mientras que un estado interno de 32 bits funciona bien con estructuras inmutables, un estado interno de 256 bits en (digamos) CityHash podría perder mucho tiempo copiando.

  1. Aleatorización: comience con un algoritmo correctamente aleatorizado (el código que @CyrusNajmabadi mostró con un valor inicial aleatorio no cuenta, ya que es probable que se elimine la aleatoriedad). Esto asegura que podamos cambiar la implementación sin problemas de compatibilidad. Aún tendríamos que ser muy sensibles a los cambios de rendimiento si cambiamos el algoritmo. Sin embargo, eso también sería una ventaja potencial, ya que podríamos tomar decisiones por arquitectura (o incluso por dispositivo). Por ejemplo, este sitio muestra que xxHash es más rápido en una Mac x64, mientras que SpookyHash es más rápido en Xbox y iPhone. Si seguimos esta ruta con la intención de cambiar los algoritmos en algún momento, es posible que debamos pensar en diseñar una API que aún tenga un rendimiento razonable si hay un estado interno de más de 64 bits.

CC @bartonjs , @terrajobst

@morganbr No hay un solo algoritmo perfecto, pero creo que tener algún algoritmo, que funciona bastante bien la mayor parte del tiempo, expuesto usando una API simple y fácil de entender es lo más útil que se puede hacer. Tener un conjunto de algoritmos además de eso, para usos avanzados está bien. Pero no debería ser la única opción, no debería tener que aprender quién es Marvin solo para poder poner mis objetos en un Dictionary .

No debería tener que saber quién es Marvin solo para poder poner mis objetos en un diccionario.

Me gusta la forma en que lo pones. También me gusta que hayas mencionado el diccionario en sí. IDictionary es algo que puede tener toneladas de implicaciones diferentes con todo tipo de cualidades diferentes (consulte las API de colecciones en muchas plataformas). Sin embargo, todavía proporcionamos un 'Diccionario' básico que hace un trabajo decente en general, aunque no sobresalga en todas las categorías.

Creo que eso es lo que un montón de gente está buscando en una biblioteca de hash. Algo que hace el trabajo, incluso si no es perfecto para todos los propósitos.

@morganbr Creo que la gente simplemente quiere una forma de escribir GetHashCode que sea mejor de lo que están haciendo hoy (generalmente una combinación de operaciones matemáticas que copiaron de algo en la web). Si puedes proporcionar una implicación básica de que las runas son buenas, la gente estará feliz. Luego, puede tener una API detrás de escena para usuarios avanzados si tienen una gran necesidad de funciones de hash específicas .

En otras palabras, las personas que escriben códigos hash hoy no sabrán ni les importará por qué querrían Spooky vs Marvin vs Murmur. Solo alguien que tenga una necesidad particular de uno de esos códigos hash específicos iría a buscar. Pero mucha gente tiene la necesidad de decir "aquí está el estado de mi objeto, proporcione una forma de producir un hash bien distribuido que sea rápido que luego pueda usar con diccionarios, y que supongo que evita que me dosifiquen si sucedo para tomar la entrada que no es de confianza, hacer un hash y almacenarla ".

@CyrusNajmabadi El problema es que si ampliamos nuestras nociones actuales de compatibilidad hacia el futuro, descubrimos que una vez que este tipo se envía, no puede cambiar nunca (a menos que descubramos que el algoritmo está terriblemente roto de una manera que "hace que todas las aplicaciones sean atacables" ).

Una vez se puede argumentar que si comienza como una forma aleatoria estable, será fácil cambiar la implementación, ya que no se puede depender del valor de ejecución a ejecución de todos modos. Pero si un par de años más tarde descubrimos que hay un algoritmo que proporciona un equilibrio igual de bueno, si no mejor, de los depósitos de hash con un mejor rendimiento en el caso general, pero crea una estructura que involucra una Lista \

Según la sugerencia de Morgan, el código que escriba hoy tendrá efectivamente las mismas características de rendimiento para siempre. Para las aplicaciones que podrían haber mejorado, esto es lamentable. Para las aplicaciones que habrían empeorado, esto es fantástico. Pero cuando encontramos el nuevo algoritmo, lo registramos y cambiamos a Roslyn (y sugerimos un cambio a ReSharper / etc.) para comenzar a generar cosas con NewAwesomeThing2019 en lugar de SomeThingThatWasConsideredAwesomeIn2018.

Cualquier cosa súper caja negra como esta solo se puede hacer una vez. Y luego nos quedamos atrapados para siempre. Luego, alguien escribe el siguiente, que tiene un mejor rendimiento promedio, por lo que hay dos implementaciones de caja negra que no sabe por qué elegiría entre ellas. Y luego ... y luego ...

Entonces, claro, es posible que no sepa por qué Roslyn / ReSharper / etc.escribió automáticamente GetHashCode para usted usando Marvin32, Murmur o FastHash, o una combinación / condicional basada en IntPtr.Size. Pero tienes el poder de investigarlo. Y tienes el poder de cambiarlo en tus tipos más adelante, a medida que se revele nueva información ... pero también te hemos dado el poder de mantenerlo igual. (Sería triste si escribiéramos esto, y en 3 años Roslyn / ReSharper / etc.están evitando explícitamente llamarlo, porque el nuevo algoritmo es mucho mejor ... por lo general).

@bartonjs ¿Qué hace que el hash sea diferente de todos los lugares donde .Net le proporciona un algoritmo de caja negra o una estructura de datos? Por ejemplo, ordenación (introsort), Dictionary (encadenamiento independiente basado en matrices), StringBuilder (lista enlazada de 8k fragmentos), la mayor parte de LINQ.

Hoy hemos analizado esto con más profundidad. Disculpas por la demora y los idas y venidas sobre este tema.

Requisitos

  • ¿Para quién es la API?

    • La API no necesita producir un hash criptográfico fuerte

    • Pero: la API debe ser lo suficientemente buena para que podamos usarla en el marco en sí (por ejemplo, en BCL y ASP.NET)

    • Sin embargo, esto no significa que tengamos que usar la API en todas partes. Está bien si hay partes del FX en las que queremos usar uno personalizado, ya sea por riesgos de seguridad / DOS o por rendimiento. Las excepciones siempre existirán .

  • ¿Cuáles son las propiedades deseadas de este hash?

    • Se utilizan todos los bits de la entrada

    • El resultado está bien distribuido

    • La API proporcionará "un" código hash, pero no garantizará un algoritmo de código hash en particular. Esto nos permite usar un algoritmo diferente más tarde o usar algoritmos diferentes en arquitecturas diferentes.

    • La API garantizará que dentro de un proceso dado, los mismos valores producirán el mismo código hash. Es probable que diferentes instancias de la misma aplicación produzcan diferentes códigos hash debido a la aleatorización. Esto nos permite asegurarnos de que los consumidores no puedan conservar los valores hash y depender accidentalmente de que sean estables en todas las ejecuciones (o peor aún, en las versiones de la plataforma).

Forma API

`` C #
// Vivirá en la asamblea central
// .NET Framework: mscorlib
// .NET Core: System.Runtime / System.Private.CoreLib
sistema de espacio de nombres
{
estructura pública HashCode
{
public static int Combine(Valor T11);
public static int Combine(Valor T1 1, valor T2 2);
public static int Combine(T1 valor1, T2 valor2, T3 valor3);
public static int Combine(T1 valor1, T2 valor2, T3 valor3, T4 valor4);
public static int Combine(T1 valor1, T2 valor2, T3 valor3, T4 valor4, T5 valor5);
public static int Combine(T1 valor1, T2 valor2, T3 valor3, T4 valor4, T5 valor5, T6 valor6);
public static int Combine(T1 valor1, T2 valor2, T3 valor3, T4 valor4, T5 valor5, T6 valor6, T7 valor7);
public static int Combine(T1 valor1, T2 valor2, T3 valor3, T4 valor4, T5 valor5, T6 valor6, T7 valor7, T8 valor8);

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

El caso más complicado es cuando el desarrollador necesita modificar la forma en que se calcula el hash. La idea es que el sitio de la llamada pase el hash deseado en lugar del objeto / valor, así:

`` C #
Cliente público de clase parcial
{
public override int GetHashCode () =>
HashCode.Combine (
Identificación,
StringComparer.OrdinalIgnoreCase.GetHashCode (Nombre),
StringComparer.OrdinalIgnoreCase.GetHashCode (Apellido),
);
}

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

Próximos pasos

Este problema quedará en juego. Para implementar la API, debemos decidir qué algoritmo usar.

@morganbr hará una propuesta para buenos candidatos. En términos generales, no queremos escribir un algoritmo hash desde cero; queremos utilizar uno conocido cuyas propiedades se comprendan bien.

Sin embargo, debemos medir la implementación para cargas de trabajo típicas de .NET y ver qué algoritmo produce buenos resultados (rendimiento y distribución). Es probable que las respuestas difieran según la arquitectura de la CPU, por lo que debemos considerar esto al medir.

@jamesqo , ¿todavía estás interesado en trabajar en esta área? En ese caso, actualice la propuesta en consecuencia.

@terrajobst , también podríamos querer public static int Combine<T1>(T1 value); . Sé que parece un poco divertido, pero proporcionaría una forma de difundir bits de algo con un espacio de hash de entrada limitado. Por ejemplo, muchas enumeraciones solo tienen unos pocos hash posibles, solo usan los pocos bits inferiores del código. Algunas colecciones se basan en el supuesto de que los hash se distribuyen en un espacio más grande, por lo que la difusión de los bits puede ayudar a que la colección funcione de manera más eficiente.

public void Add(string value, StrinComparison comparison);

Nit: El parámetro StringComparison debe llamarse comparisonType para que coincida con la denominación utilizada en todos los demás lugares donde se utilice StringComparison como parámetro.

Los criterios que nos ayudarían a elegir algoritmos serían:

  1. ¿Tiene el algoritmo un buen efecto de avalancha? Es decir, ¿cada bit de entrada tiene un 50% de posibilidades de cambiar cada bit de salida? Este sitio tiene un estudio de varios algoritmos populares.
  2. ¿El algoritmo es rápido para pequeñas entradas? Dado que HashCode.Combine generalmente manejará 8 entradas o menos, el tiempo de inicio puede ser más importante que el rendimiento. Este sitio tiene un interesante conjunto de datos para empezar. Aquí también es donde podemos necesitar diferentes respuestas para diferentes arquitecturas u otros pivotes (SO, AoT vs JIT, etc.).

Lo que realmente nos gustaría ver son los números de rendimiento de los candidatos escritos en C # para que podamos estar razonablemente seguros de que sus características se mantendrán para .NET. Si escribe un candidato y no lo elegimos para esto, seguirá siendo un trabajo útil cada vez que reúna la propuesta de API para la API hash no criptográfica.

Aquí hay algunos candidatos que creo que vale la pena evaluar (pero siéntase libre de proponer otros):

  • Marvin32 (ya tenemos una implementación de C # aquí ). Sabemos que es lo suficientemente rápido para String.GetHashCode y creemos que es resistente a HashDoS
  • xxHash32 (el algoritmo más rápido en x86 aquí que tiene la mejor calidad según SMHasher)
  • FarmHash (el más rápido en x64 aquí . No he encontrado un buen indicador de calidad para él. Sin embargo, este podría ser difícil de escribir en C #)
  • xxHash64 (truncado a 32 bits) (Este no es un claro ganador de velocidad, pero podría ser fácil de hacer si ya tenemos xxHash32)
  • SpookyHash (tiende a funcionar bien en conjuntos de datos más grandes)

Es una pena que los métodos Add no puedan tener un tipo de retorno de ref HashCode y devuelvan ref this para que puedan usarse de manera fluida,

¿Las devoluciones de readonly ref permitirían esto? / cc @jaredpar @VSadov

ADVERTENCIA: Si alguien elige la implementación de hash de la base de código existente en algún lugar de Internet, mantenga el enlace a la fuente y verifique la licencia (también tendremos que hacerlo).

Si la licencia no es compatible, es posible que debamos escribir el algoritmo desde cero.

En mi opinión, el uso de los métodos Add debería ser muy poco común. Será para escenarios muy avanzados, y la necesidad de poder ser 'fluido' realmente no existirá.

Para los casos de uso comunes para el 99% de todos los casos de código de usuario, uno debería poder usar => HashCode.Combine(...) y estar bien.

@morganbr

también podríamos querer public static int Combine<T1>(T1 value); . Sé que parece un poco divertido, pero proporcionaría una forma de difundir bits de algo con un espacio de hash de entrada limitado.

Tener sentido. Lo he añadido.

@justinvp

Nit: El parámetro StringComparison debe llamarse comparisonType para que coincida con la denominación utilizada en todos los demás lugares donde se utilice StringComparison como parámetro.

Reparado.

@CyrusNajmabadi

En mi opinión, el uso de los métodos Add debería ser extremadamente poco común. Será para escenarios muy avanzados, y la necesidad de poder ser 'fluido' realmente no existirá.

Acordado.

@benaadams - re: ref que devuelve this de Add - no, this no puede ser devuelto por ref en los métodos struct ya que puede ser un rValue o un temp.

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

// r se refiere a alguna variable aquí. ¿Cuál? ¿Cuál es el alcance / vida útil?
r = SomethingElse ();
''

En caso de que sea útil para fines de comparación, hace algunos años porté la función hash de fuente C ) a C # aquí .

Me pregunto acerca de las colecciones:

@terrajobst

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

¿Por qué hay una sobrecarga para las matrices, pero no una para las colecciones generales (es decir, IEnumerable<T> )?

Además, ¿no será confuso que HashCode.Combine(array) y hashCode.Add((object)array) comporten de una manera (use la igualdad de referencia) y hashCode.Add(array) comporten de otra manera (combina códigos hash de los valores en la matriz)?

@CyrusNajmabadi

Para los casos de uso común para el 99% de todos los casos de código de usuario, uno debería poder usar => HashCode.Combine(...) y estar bien.

Si el objetivo es realmente poder usar Combine en el 99% de los casos de uso (y no, digamos, el 80%), entonces Combine alguna manera no debería admitir colecciones hash basadas en los valores en la colección? ¿Quizás debería haber un método separado que haga eso (ya sea un método de extensión o un método estático en HashCode )?

Si Agregar es un escenario de energía, ¿deberíamos asumir que el usuario debe elegir entre Object.GetHashCode y combinar elementos individuales de colecciones? Si pudiera ayudar, podríamos considerar cambiar el nombre de la matriz (y posibles versiones de IEnumerable). Algo como:
c# public void AddEnumerableHashes<T>(IEnumerable<T> enumerable); public void AddEnumerableHashes<T>(T[] array); public void AddEnumerableHashes<T>(T[] array, int index, int length);
Me pregunto si también necesitaríamos sobrecargas con IEqualityComparers.

Propuesta: hacer que la estructura del constructor implemente IEnumerable para admitir la sintaxis del inicializador de la colección:

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

Esto es mucho más elegante que llamar a Add() manualmente (en particular, no necesita una variable temporal) y aún no tiene asignaciones.

más detalles

@SLaks Quizás esa sintaxis más agradable podría esperar a https://github.com/dotnet/csharplang/issues/455 (suponiendo que la propuesta tuviera soporte), de modo que HashCode no tendría que implementar IEnumerable falsos

Decidimos no anular GetHashCode () para producir el código hash, ya que esto sería extraño, tanto desde el punto de vista del nombre como desde el punto de vista del comportamiento (GetHashCode () debería devolver el código hash del objeto, no el que se está calculando).

Me parece extraño que GetHashCode no devuelva el código hash calculado. Creo que esto confundirá a los desarrolladores. Por ejemplo, @SLaks ya lo usó en su propuesta en lugar de usar ToHashCode .

@justinvp Si GetHashCode() no va a devolver el código hash calculado, probablemente debería estar marcado como [Obsolete] y [EditorBrowsable(Never)] .

Por otro lado, no veo el daño en devolver el código hash calculado.

@terrajobst

Decidimos no anular GetHashCode() para producir el código hash, ya que esto sería extraño, tanto desde el punto de vista del nombre como desde el punto de vista del comportamiento ( GetHashCode() debería devolver el código hash del objeto, no el siendo calculado).

Sí, GetHashCode() debería devolver el código hash del objeto, pero ¿hay alguna razón por la que los dos códigos hash deberían ser diferentes? Aún es correcto, ya que dos instancias de HashCode con el mismo estado interno devolverán el mismo valor de GetHashCode() .

@terrajobst Acabo de ver tu comentario. Perdóname por la respuesta tardía, tardé en mirar la notificación porque pensé que solo sería más de ida y vuelta que no iba a ninguna parte. ¡Me alegra ver que ese no es el caso! : tada:

Estaría encantado de tomar esto y hacer la medición de rendimiento / distribución (supongo que eso es lo que quiere decir con "interesado en trabajar en esta área"). Sin embargo, dame un segundo para terminar de leer todos los comentarios aquí.

@terrajobst

Podemos cambiar

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

para

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

? Cambié el nombre de Add -> AddRange para evitar el comportamiento mencionado por @svick . Eliminé las sobrecargas byte ya que podemos especializarnos usando typeof(T) == typeof(byte) dentro del método si necesitamos hacer algo específico por byte. Además, cambié value -> values y length -> count . También tiene sentido tener una sobrecarga de comparadores.

@terrajobst ¿Puedes recordarme por qué?

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

es necesario cuando tenemos

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

?

@svick

@justinvp Si GetHashCode () no va a devolver el código hash calculado, probablemente debería estar marcado como [Obsolete] y [EditorBrowsable (Never)].

: +1:

@terrajobst ¿Podemos volver a tener una conversión implícita de HashCode -> int , entonces no hay método ToHashCode ? editar: ToHashCode está bien. Vea la respuesta de @CyrusNajmabadi a continuación.

@jamesqo StringComparison es una enumeración.
Sin embargo, la gente podría usar el equivalente StringComparer lugar.

¿Podemos volver a tener una conversión implícita de HashCode -> int, entonces no hay método ToHashCode?

Discutimos esto y decidimos no hacerlo en la reunión. El problema es que cuando el usuario obtiene el 'int' final, a menudo se realiza un trabajo adicional. es decir, las partes internas del código hash a menudo realizarán un paso de finalización y pueden restablecerse a un estado nuevo. Que eso suceda con una conversión implícita sería extraño. Si hiciste esto:

HashCode hc = ...

int i1 = hc;
int i2 = hc;

Entonces podrías obtener diferentes resultados.

Por esa razón, tampoco nos gusta la conversión explícita (ya que la gente no piensa en las conversiones como un estado interno cambiante).

Con un método podemos documentar explícitamente que esto está sucediendo. Incluso potencialmente podemos nombrarlo para transmitirlo. es decir, "ToHashCodeAndReset" (aunque decidimos no hacerlo). Pero al menos el método puede tener documentación clara que el usuario puede ver en cosas como intellisense. Ese no es realmente el caso de las conversiones.

Eliminé las sobrecargas de bytes ya que podemos especializarnos usando typeof (T) == typeof (byte)

IIRC hubo cierta preocupación acerca de que esto no estuviera bien desde la perspectiva del JIT. Pero eso puede haber sido solo para los casos de "typeof ()" sin tipo de valor. Siempre que el jit haga efectivamente lo correcto para los casos de tipo de valor typeof (), entonces eso debería ser bueno.

@CyrusNajmabadi No sabía que la conversión a int podría implicar un estado mutante. ToHashCode es entonces.

Para aquellos que piensan en la perspectiva criptográfica: http://tuprints.ulb.tu-darmstadt.de/2094/1/thesis.lehmann.pdf

@terrajobst , ¿ha tenido tiempo de leer mis comentarios (comenzando desde aquí ) y decidir si aprueba la forma de API modificada? Si es así, creo que esto se puede marcar como api aprobado / disponible y podemos comenzar a decidir sobre un algoritmo hash.

@blowdart , ¿alguna parte en particular de eso te gustaría resaltar?

Puede que no haya sido demasiado explícito al respecto anteriormente, pero los únicos hashes no criptográficos que no conozco de las interrupciones de HashDoS son Marvin y SipHash. Es decir, incluso sembrar (digamos) Murmur con un valor aleatorio aún se puede romper y usar para un DoS.

Ninguno, simplemente lo encontré interesante, y creo que los documentos para esto deberían decir "No para usar en códigos hash que se generan a través de algoritmos criptográficos".

Decisiones

  • Deberíamos eliminar todos los métodos AddRange porque el escenario no está claro. Es poco probable que las matrices aparezcan con mucha frecuencia. Y una vez que se involucran matrices más grandes, la pregunta es si el cálculo debe almacenarse en caché. Ver el bucle for en el lado de la llamada deja en claro que debes pensar en eso.
  • Tampoco queremos agregar IEnumerable sobrecargas a AddRange porque se asignarían.
  • No creemos que necesitemos la sobrecarga de Add que requiere string y StringComparison . Sí, es probable que sean más eficientes que llamar a través del IEqualityComparer , pero podemos solucionarlo más tarde.
  • Creemos que marcar GetHashCode como obsoltete con error es una buena idea, pero iríamos un paso más allá y también nos esconderíamos de IntelliSense.

Esto nos deja con:

`` C #
// Vivirá en la asamblea central
// .NET Framework: mscorlib
// .NET Core: System.Runtime / System.Private.CoreLib
sistema de espacio de nombres
{
estructura pública HashCode
{
public static int Combine(Valor T11);
public static int Combine(Valor T1 1, valor T2 2);
public static int Combine(T1 valor1, T2 valor2, T3 valor3);
public static int Combine(T1 valor1, T2 valor2, T3 valor3, T4 valor4);
public static int Combine(T1 valor1, T2 valor2, T3 valor3, T4 valor4, T5 valor5);
public static int Combine(T1 valor1, T2 valor2, T3 valor3, T4 valor4, T5 valor5, T6 valor6);
public static int Combine(T1 valor1, T2 valor2, T3 valor3, T4 valor4, T5 valor5, T6 valor6, T7 valor7);
public static int Combine(T1 valor1, T2 valor2, T3 valor3, T4 valor4, T5 valor5, T6 valor6, T7 valor7, T8 valor8);

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

}
''

Próximos pasos: el problema está en juego: para implementar la API que necesitamos con varios algoritmos candidatos como experimentos, consulte https://github.com/dotnet/corefx/issues/14354#issuecomment -305028686 para obtener una lista, para que podamos decidir qué algoritmo tomar (según las medidas de rendimiento y distribución, probablemente una respuesta diferente por arquitectura de CPU).

Complejidad: grande

Si alguien está interesado en recogerlo, envíenos un mensaje. Incluso podría haber espacio para que varias personas trabajen juntas en él. ( @jamesqo tiene una opción prioritaria ya que invirtió más y más tiempo en el problema)

@karelz A pesar de mi comentario anterior , he cambiado de opinión porque no creo que tenga las calificaciones para elegir el mejor algoritmo hash. Miré algunas de las bibliotecas que @morganbr enumeró y me di cuenta de que la implementación es bastante compleja , por lo que no puedo traducirla fácilmente a C # para probar por mí mismo. Tengo poca experiencia en C ++, por lo que también me resultaría difícil instalar la biblioteca y escribir una aplicación de prueba.

Sin embargo, no quiero que esto permanezca en la lista de opciones para siempre. Si nadie lo retoma dentro de una semana, consideraré publicar una pregunta en Programmers SE o Reddit.

No lo he colocado en la banca (ni lo he optimizado), pero aquí hay una implementación básica del algoritmo hash Murmur3 que utilizo en varios de mis proyectos personales: https://gist.github.com/tannergooding/0a12559d1a912068b9aeb4b9586aad7f

Siento que la solución más óptima aquí será cambiar dinámicamente el algoritmo hash en función del tamaño de los datos de entrada.

Por ejemplo: Mumur3 (y otros) son muy rápidos para grandes conjuntos de datos y proporcionan una gran distribución, pero pueden funcionar "mal" (en cuanto a velocidad, no en cuanto a distribución) para conjuntos de datos más pequeños.

Me imagino que deberíamos hacer algo como: Si el recuento total de bytes es menor que X, haga el algoritmo A; de lo contrario, utilice el algoritmo B. Este seguirá siendo determinista (por ejecución), pero nos permitirá proporcionar velocidad y distribución en función del tamaño real de los datos de entrada.

Probablemente también valga la pena señalar que varios de los algoritmos mencionados tienen implementaciones diseñadas específicamente para instrucciones SIMD, por lo que una solución de mayor rendimiento probablemente involucraría un FCALL en algún nivel (como se hace con algunas de las implementaciones de BufferCopy) o puede involucrar tomar una dependencia en System.Numerics.Vector .

@jamesqo , nos complace ayudarlo a tomar decisiones; con lo que necesitamos más ayuda son los datos de rendimiento para las implementaciones candidatas (idealmente C #, aunque como señala @tannergooding , algunos algoritmos necesitan un soporte especial para el compilador). Como mencioné anteriormente, si crea un candidato que no es elegido, probablemente lo usaremos más adelante, así que no se preocupe por el desperdicio de trabajo.

Sé que existen puntos de referencia para varias implementaciones, pero creo que es importante tener una comparación usando esta API y un rango probable de entradas (por ejemplo, estructuras con 1-10 campos).

@tannergooding , ese tipo de adaptabilidad podría ser más

Además, dado que el rango de entradas más probable es de 4 a 32 bytes ( Combine`1 - Combine`8 ), es de esperar que no haya grandes cambios de rendimiento en ese rango.

ese tipo de adaptabilidad puede ser más eficaz, pero no veo cómo funcionaría con el método Add, ya que no sabe cuántas veces se llamará.

Personalmente, no estoy convencido de que la forma de la API sea adecuada para el hash de propósito general (sin embargo, está cerca) ...

Actualmente estamos exponiendo los métodos Combine para la construcción estática. Si están destinados a combinar todas las entradas y producir un código hash finalizado, entonces el nombre es 'pobre' y algo como Compute podría ser más apropiado.

Si estamos exponiendo métodos Combine , deberían simplemente mezclar todas las entradas y los usuarios deberían llamar a un método Finalize que toma la salida de la última combinación, así como el número total de bytes que fueron combinados para producir un código hash finalizado (la finalización de un código hash es importante ya que es lo que provoca la avalancha de bits).

Para el patrón del constructor, estamos exponiendo un método Add y ToHashCode . No está claro si el método Add está destinado a almacenar los bytes y solo combinar / finalizar en la llamada a ToHashCode (en cuyo caso podemos elegir el algoritmo correcto dinámicamente) o si lo son destinado a ser combinado sobre la marcha, debe quedar claro que este es el caso (y que la implementación debe realizar un seguimiento interno del tamaño total de bytes combinados).

Para cualquiera que busque un punto de partida menos complicado, pruebe xxHash32. Es probable que se traduzca con bastante facilidad a C # (la gente lo ha hecho ).

Todavía estoy probando localmente, pero veo las siguientes tasas de rendimiento para mi implementación de C # de Murmur3.

Estos son para los métodos combinados estáticos para 1-8 entradas:

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

Mi implementación asume que se debe llamar GetHashCode para cada entrada y que el valor calculado se debe finalizar antes de ser devuelto.

Combiné los valores int , ya que son los más sencillos de probar.

Para calcular el rendimiento, ejecuté 10,001 iteraciones, descartando la primera iteración como ejecución de 'calentamiento'.

En cada iteración, ejecuto 10,000 sub-iteraciones donde llamo HashCode.Combine , pasando el resultado de la sub-iteración anterior como el primer valor de entrada en la siguiente iteración.

Luego hago un promedio de todas las iteraciones para obtener el tiempo promedio transcurrido, y luego lo divido por el número de sub-iteraciones ejecutadas por ciclo para obtener el tiempo promedio por llamada. Luego calculo el número de llamadas que se pueden hacer por segundo y lo multiplico por el número de bytes combinados para calcular el rendimiento real.

Limpiará el código y lo compartirá en un momento.

@tannergooding , eso suena como un gran progreso. Para asegurarse de que está obteniendo las medidas correctas, la intención de la API es que una llamada a HashCode.Combine(a, b) sea ​​equivalente a llamar

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

En ambos casos, los datos deben introducirse en el mismo estado de hash interno y el hash debe finalizarse una vez al final.

👍

Eso es efectivamente lo que está haciendo el código que escribí. La única diferencia es que efectivamente alineo todo el código (no hay necesidad de asignar new HashCode() y rastrear el número de bytes combinados ya que es constante).

@morganbr. Prueba de implementación + rendimiento para Murmur3: https://gist.github.com/tannergooding/89bd72f05ab772bfe5ad3a03d6493650

MurmurHash3 se basa en el algoritmo descrito aquí: https://github.com/aappleby/smhasher/wiki/MurmurHash3 , repo dice que es MIT

Trabajando en xxHash32 (Cláusula BSD-2 - https://github.com/Cyan4973/xxHash/blob/dev/xxhash.c) y SpookyHash (Dominio público - http://www.burtleburtle.net/bob/hash /spooky.html) variantes

@tannergooding Una

@jamesqo , podría estar equivocado, pero estoy bastante seguro de que la vulnerabilidad se aplicó a Murmur2 y no a Murmur3.

En cualquier caso, estoy implementando varios algoritmos para que podamos obtener resultados de rendimiento para C #. La distribución y otras propiedades de estos algoritmos son bastante conocidas, por lo que podemos elegir cuál es el mejor más adelante 😄

Vaya, olvidé vincular al artículo: http://emboss.github.io/blog/2012/12/14/breaking-murmur-hash-flooding-dos-reloaded/.

@tannergooding OK. Suena justo: +1:

@tannergooding , eché un vistazo a su implementación de Murmur3 y, en general, se ve bien y probablemente bastante bien optimizada. Para asegurarme de que entiendo correctamente, ¿está utilizando el hecho de que el valor combinado y el estado interno de Murmur son ambos de 32 bits? Esa es probablemente una optimización bastante buena para este caso y explica parte de mi confusión anterior.

Si tuviéramos que adoptarlo, podría necesitar un par de ajustes (aunque probablemente no harán una gran diferencia en las mediciones de rendimiento):

  • Combinartodavía debería llamar a CombineValue en value1
  • Las primeras llamadas a CombineValue deben tener una semilla aleatoria
  • ToHashCode debería restablecer _bytesCombined y _combinedValue

Mientras tanto, mientras anhelo esta API, ¿qué tan malo es para mí implementar GetHashCode a través de (field1, field2, field3).GetHashCode() ?

@ jnm2 , el combinador de código hash ValueTuple tiende a ordenar sus entradas en el código hash (y descartar las menos recientes). Para un par de campos y una tabla hash que se divide por un número primo, es posible que no se dé cuenta. Para muchos campos o una tabla hash que se divide por una potencia de dos, la entropía del último campo que inserte tendrá la mayor influencia sobre si tiene colisiones (por ejemplo, si su último campo es un bool o un int pequeño, Probablemente tenga muchas colisiones, si es una guía, probablemente no las tendrá).

ValueTuple tampoco funciona bien con campos que son todos 0.

En una nota al margen, tuve que dejar de trabajar en otras implementaciones (tengo un trabajo de mayor prioridad). No estoy seguro de cuándo podré recuperarlo.

Entonces, si eso no es lo suficientemente bueno para un tipo estructurado, ¿por qué es lo suficientemente bueno para una tupla?

@ jnm2 , esa es una de las razones por las que vale la pena construir esta característica, para que podamos reemplazar los hash deficientes en todo el marco.

Amplia tabla de funciones hash con características de rendimiento y calidad:
https://github.com/leo-yuriev/t1ha

@arespr Creo que el equipo está buscando una implementación C # de las funciones hash. Sin embargo, gracias por compartir.

@tannergooding ¿Aún no puede recuperar este problema? Si es así, publicaré en Reddit / Twitter que estamos buscando un experto en hash.

editar: Hice una publicación en Reddit. https://www.reddit.com/r/csharp/comments/6qsysm/looking_for_hash_expert_to_help_net_core_team/?ref=share&ref_source=link

@jamesqo , tengo algunas cosas de mayor prioridad en mi plato y no podré llegar a esto en las próximas 3 semanas.

Además, las medidas actuales estarán limitadas por lo que podemos codificar actualmente en C #, sin embargo, si / cuando esto se convierta en algo (https://github.com/dotnet/designs/issues/13), es probable que las medidas cambien algo. ;)

Además, las medidas actuales estarán limitadas por lo que podemos codificar actualmente en C #, sin embargo, si / cuando esto se convierta en algo (dotnet / designs # 13), es probable que las medidas cambien algo;)

Eso está bien, siempre podemos cambiar el algoritmo hash una vez que los intrínsecos estén disponibles, encapsular / aleatorizar el código hash nos permite hacer eso. Solo estamos buscando algo que ofrezca la mejor compensación de rendimiento / distribución para el tiempo de ejecución en su estado actual.

@jamesqo , gracias por buscar gente para ayudar. Estaríamos encantados de que alguien que no sea un experto en hash trabaje en esto también; realmente solo necesitamos a alguien que pueda transferir algunos algoritmos a C # desde otros lenguajes o diseños y luego realizar mediciones de rendimiento. Una vez que hayamos elegido a los candidatos, nuestros expertos harán lo que hacemos con cualquier cambio: revisar el código para verificar su corrección, rendimiento, seguridad, etc.

¡Hola! Acabo de leer la discusión, y al menos a mí me parece que el caso está fuertemente cerrado a favor del murmur3-32 PoC. Lo cual, por cierto, me parece una muy buena elección, y recomendaría no gastar más trabajo innecesario (pero tal vez incluso eliminar los miembros .Add() ...).

Pero en el improbable caso de que alguien quiera continuar con más trabajo de rendimiento, podría proporcionar algún código para xx32, xx64, hsip13 / 24, seahash, murmur3-x86 / 32 (e integré el impl marvin32 de arriba), y (aún no optimizado) sip13 / 24, spookyv2. Algunas versiones de City parecen bastante fáciles de transferir, en caso de que surja la necesidad. Ese proyecto medio abandonado tenía en mente un caso de uso ligeramente diferente, por lo que no hay una clase HashCode con la API propuesta; pero para la evaluación comparativa no debería importar mucho.

Definitivamente no está listo para producción: el código aplica generosas cantidades de fuerza bruta como copia de pasta, expansión cancerosa de agresivo en línea e inseguro; la endianess no existe, ni tampoco las lecturas no alineadas. Incluso las pruebas contra los vectores de prueba ref-impl son eufemísticamente hablando "incompletas".

Si esto es de alguna ayuda, debería encontrar suficiente tiempo durante las próximas dos semanas para solucionar los problemas más graves y hacer que el código y algunos resultados preliminares estén disponibles.

@gimpf

Acabo de leer la discusión, y al menos a mí me parece que el caso está fuertemente cerrado a favor del murmur3-32 PoC. Lo cual, por cierto, me parece una muy buena elección, y recomendaría no gastar más trabajo innecesario

No, la gente todavía no está a favor de Murmur3. Queremos asegurarnos de que estamos eligiendo el mejor algoritmo absoluto en términos de equilibrio entre rendimiento / distribución, por lo que no podemos dejar piedra sin remover.

Pero en el improbable caso de que alguien quiera continuar con más trabajo de rendimiento, podría proporcionar algún código para xx32, xx64, hsip13 / 24, seahash, murmur3-x86 / 32 (e integré el impl marvin32 de arriba), y (aún no optimizado) sip13 / 24, spookyv2. Algunas versiones de City parecen bastante fáciles de transferir, en caso de que surja la necesidad.

¡Sí, por favor! Queremos recopilar código para tantos algoritmos como sea posible para probarlos. Cada nuevo algoritmo que pueda contribuir es valioso. Le agradeceríamos mucho que también pudiera portar los algoritmos de City.

Definitivamente no está listo para producción: el código aplica generosas cantidades de fuerza bruta como copia de pasta, expansión cancerosa de agresivo en línea e inseguro; la endianess no existe, ni tampoco las lecturas no alineadas. Incluso las pruebas contra los vectores de prueba ref-impl son eufemísticamente hablando "incompletas".

Está bien. Simplemente traiga el código y alguien más podrá encontrarlo si surge la necesidad.

Si esto es de alguna ayuda, debería encontrar suficiente tiempo durante las próximas dos semanas para solucionar los problemas más graves y hacer que el código y algunos resultados preliminares estén disponibles.

¡Si, eso sería muy bueno!

@jamesqo Ok, dejaré una nota una vez que tenga algo que mostrar.

@gimpf, eso suena realmente genial y nos encantaría saber sobre su progreso a medida que avanza (¡no es necesario esperar hasta que comience a trabajar con todos los algoritmos!). No estar listo para producción está bien siempre que crea que el código produce resultados correctos y que el rendimiento es una buena representación de lo que veríamos en una implementación lista para producción. Una vez que seleccionamos a los candidatos, podemos trabajar con usted para lograr implementaciones de alta calidad.

No he visto un análisis de cómo la entropía de seahash se compara con otros algoritmos. ¿Tiene alguna sugerencia sobre eso? Tiene interesantes compensaciones de rendimiento ... la vectorización suena rápido, pero la aritmética modular suena lenta.

@morganbr Tengo un teaser listo.

Acerca de SeaHash : No, todavía no conozco la calidad; en caso de que el rendimiento sea interesante, lo agregaría a SMHasher. Al menos el autor afirma que es bueno (usándolo para sumas de comprobación en un sistema de archivos), y también afirma que no se tira entropía durante la mezcla.

Acerca de los hashes y los puntos de referencia : Proyecto Haschisch.Kastriert , página wiki con los primeros resultados de los puntos de referencia comparando xx32, xx64, hsip13, hsip24, marvin32, sea y murmur3-32.

Algunas advertencias importantes:

  • Esta fue una prueba de banco muy rápida con configuraciones de baja precisión.
  • Las implementaciones aún no están terminadas y aún faltan algunos contendientes. Las implementaciones de Streaming (tal cosa sería necesaria para un soporte sensato .Add ()) necesitan una optimización real.
  • SeaHash actualmente no está usando una semilla.

Primeras impresiones:

  • para mensajes grandes, xx64 es la más rápida de las implementaciones enumeradas (alrededor de 3.25 bytes por ciclo, hasta donde tengo entendido, o 9.5 GiB / s en mi computadora portátil)
  • para mensajes cortos, nada es genial, pero murmur3-32, y (sorprendentemente) seahash tienen una ventaja, pero esto último probablemente se explica porque seahash aún no usa una semilla.
  • el "punto de referencia" para acceder a un HashSet<> necesita trabajo, ya que todo está casi dentro del error de medición (he visto diferencias más grandes, pero aún no vale la pena hablar de ellas)
  • al combinar códigos hash, el PoC murmur-3A es alrededor de 5 a 20 veces más rápido que lo que tenemos aquí
  • algunas abstracciones en C # son muy caras; eso hace que comparar algoritmos hash sea más molesto de lo necesario.

Te volveré a escribir una vez que haya mejorado un poco la situación.

@gimpf , ¡es un comienzo fantástico! Eché un vistazo al código y los resultados y tengo algunas preguntas.

  1. Sus resultados muestran que SimpleMultiplyAdd es aproximadamente 5 veces más lento que Murmur3a de @tannergooding. Eso parece extraño, ya que Murmur tiene más trabajo que hacer que multiplicar + agregar (aunque admitiré que rotar es una operación más rápida que sumar). ¿Es posible que sus implementaciones tengan una ineficiencia común que no esté en esa implementación de Murmur o debería leer esto como implementaciones personalizadas que tienen una gran ventaja sobre las de propósito general?
  2. Tener resultados para 1, 2 y 4 combinaciones es bueno, pero esta API sube a 8. ¿Sería posible obtener resultados para eso también o eso causa demasiada duplicación?
  3. Vi que se ejecutó en X64, por lo que estos resultados deberían ayudarnos a elegir nuestro algoritmo X64, pero otros puntos de referencia sugieren que los algoritmos pueden diferir dramáticamente entre X86 y X64. ¿Es fácil para usted obtener también resultados de X86? (En algún momento, también necesitaríamos obtener ARM y ARM64, pero definitivamente pueden esperar)

Los resultados de su HashSet son particularmente interesantes. Si se mantienen, ese es un caso posible para preferir una mejor entropía en lugar de un tiempo hash más rápido.

@morganbr Este fin de semana fue más

Sobre sus preguntas:

  1. Sus resultados muestran que SimpleMultiplyAdd es aproximadamente 5 veces más lento que Murmur3a de @tannergooding. Eso parece extraño ...

Me estaba preguntando a mí mismo. Eso fue un error de copiar / pegar, SimpleMultiplyAdd siempre combinaba cuatro valores ... Además, al reordenar algunas declaraciones, el combinador de agregar y multiplicar se volvió un poco más rápido (~ 60% más de rendimiento).

¿Es posible que sus implementaciones tengan una ineficiencia común que no esté en esa implementación de Murmur o debería leer esto como implementaciones personalizadas que tienen una gran ventaja sobre las de propósito general?

Es probable que me pierda algunas cosas, pero parece que las implementaciones de propósito general de .NET no se pueden usar para este caso de uso. He escrito métodos de estilo Combine para todos los algoritmos, y la combinación de código hash wrt funciona _mucho_ mejor que los de propósito general.

Sin embargo, incluso esas implementaciones siguen siendo demasiado lentas; se necesita más trabajo. El rendimiento de .NET en esta área es absolutamente opaco para mí; agregar o eliminar una copia de una variable local puede cambiar fácilmente el rendimiento en un factor de dos. Es probable que no pueda proporcionar implementaciones que estén lo suficientemente bien optimizadas con el fin de seleccionar la mejor opción.

  1. Tener resultados para 1, 2 y 4 combinaciones es bueno, pero esta API sube a 8.

He ampliado los puntos de referencia de la cosechadora. Sin sorpresas en ese frente.

  1. Vi que corría en X64 (...), ¿es fácil para usted obtener también resultados de X86?

Una vez lo fue, pero luego lo porté a .NET Standard. Ahora estoy en el infierno de dependencias, y solo funcionan los puntos de referencia de .NET Core 2 y CLR de 64 bits. Esto se puede resolver con bastante facilidad una vez que haya resuelto los problemas actuales.

¿Crees que esto lo hará en la versión v2.1?

@gimpf No ha publicado en un tiempo, ¿tiene una actualización de progreso en sus implementaciones? : smiley:

@jamesqo He arreglado algunos puntos de referencia que causaron resultados extraños y agregué City32, SpookyV2, Sip13 y Sip24 a la lista de algoritmos disponibles. Los Sips son tan rápidos como se esperaba (en relación con el rendimiento de xx64), City y Spooky no lo son (lo mismo sigue siendo cierto para SeaHash).

Para combinar códigos hash, Murmur3-32 todavía parece una buena apuesta, pero todavía tengo que hacer una comparación más exhaustiva.

En otra nota, la API de transmisión (.Add ()) tiene el desafortunado efecto secundario de eliminar algunos algoritmos hash de la lista de candidatos. Dado que el rendimiento de dicha API también es cuestionable, es posible que desee reconsiderar si ofrecerla desde el principio.

Si se evitara la parte .Add() , y dado que el combinador de hash está usando una semilla, no creo que haya ningún daño en limpiar el combinador de tg, crear un pequeño conjunto de pruebas y terminar. Como solo tengo unas pocas horas cada fin de semana, y la optimización del rendimiento es algo tediosa, hacer que la versión dorada podría demorarse un poco ...

@gimpf , eso suena como un gran progreso. ¿Tiene una tabla de resultados a mano para que podamos ver si hay suficiente para tomar una decisión y seguir adelante?

@morganbr He actualizado mis resultados de evaluación comparativa .

Por ahora, solo tengo resultados de 64 bits en .NET Core 2. Para esa plataforma, City64 sin semilla es el más rápido en todos los tamaños. Al incorporar una semilla, XX-32 se vincula con Murmur-3-32. Afortunadamente, estos son los mismos algoritmos que tienen la reputación de ser rápidos para plataformas de 32 bits, pero obviamente debemos verificar que eso también sea válido para mi implementación. Los resultados parecen ser representativos del rendimiento del mundo real, excepto que Sea y SpookyV2 parecen inusualmente lentos.

Deberá considerar cuánto necesita realmente protección hash-dos para los combinadores de código hash. Si la siembra solo es necesaria para hacer que el hash sea obviamente inutilizable para la persistencia, city64 una vez XOR con una semilla de 32 bits sería una mejora. Como esta utilidad solo está ahí para combinar hashes (y no reemplazar, por ejemplo, el código hash para cadenas, o ser un hasher directo para matrices de enteros, etc.), eso podría ser lo suficientemente bueno.

Si OTOH cree que lo necesita, le alegrará ver que Sip13 suele ser menos del 50% más lento que XX-32 (en plataformas de 64 bits), pero ese resultado probablemente será significativamente diferente para las aplicaciones de 32 bits.

No sé cuánto es relevante para corefx, pero agregué resultados de LegacyJit 32bit (con FW 4.7).

Me gustaría decir que los resultados son ridículamente lentos. Sin embargo, como ejemplo, a 56 MiB / s frente a 319 MiB / s no me estoy riendo (eso es Sip, es lo que más le falta a la optimización de rotación a la izquierda). Creo recordar por qué cancelé mi proyecto de algoritmo hash .NET en enero ...

Entonces, aún falta RyuJit-32bit y (con suerte) dará resultados muy diferentes, pero para LegacyJit-x86, Murmur-3-32 gana fácilmente, y solo City-32 y xx-32 pueden acercarse. Murmur todavía tiene un mal rendimiento de solo alrededor de 0.4 a 1.1 GB / s en lugar de 0.6 a 2 GB / s (en la misma máquina), pero al menos está en el estadio correcto.

Voy a ejecutar los puntos de referencia en algunas de mis cajas esta noche y publicar los resultados (Ryzen, i7, Xeon, A10, i7 Mobile y creo que un par de otros).

@tannergooding @morganbr Algunas actualizaciones interesantes e importantes.

Importante primero:

  • Arreglé algunas implementaciones combinadas que producían valores hash incorrectos.
  • La suite de referencia ahora trabaja más duro para evitar el plegado constante. City64 era susceptible (al igual que murmur-3-32 en el pasado). No significa que ahora entienda todos los resultados, pero son mucho más plausibles.

Cosas bonitas:

  • Las implementaciones de combinador ahora están disponibles para todas las sobrecargas de 1 a 8 argumentos, incluidas las implementaciones desenrolladas manualmente algo más engorrosas para xx / city.
  • Las pruebas y los puntos de referencia también los comprueban. Dado que muchos algoritmos hash tienen mensajes de bytes bajos en mayúsculas y minúsculas especiales, esas mediciones pueden ser de interés.
  • Puntos de referencia de ejecución simplificados para múltiples objetivos (Core vs. FW).

Para ejecutar una suite en todas las implementaciones principales para combinar códigos hash, incluidos "Empty" (gastos generales puros) y "multiply-add" (versión de velocidad optimizada de la famosa respuesta SO):

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

(_La ejecución de los puntos de referencia de 32 bits Core convenientemente parece requerir la versión preliminar de BenchmarkDotNet (o tal vez una configuración de solo 32 bits más usando el banco de pruebas basado en Core). Entonces debería funcionar usando -j: core_x86, con suerte) _

Resultados : Después de todas las correcciones de errores, xx32 parece ganar para todas las sobrecargas con RyuJIT de 64 bits, en Windows 10 en un Haswell i7 móvil, en una ejecución "rápida". Entre Sips y marvin32, Sip-1-3 siempre gana. Sip-1-3 es aproximadamente 4 veces más lento que xx32, que nuevamente es aproximadamente 2 veces más lento que un combinador primitivo de suma múltiple. Aún faltan los resultados de 32bit Core, pero estoy más o menos esperando una versión estable de BenchmarkDotNet que me resuelva ese problema.

(Editar) Acabo de agregar una ejecución rápida de un punto de referencia para acceder a un conjunto de hash . Obviamente, esto depende mucho más de los detalles que los puntos de referencia µ anteriores, pero es posible que desee echarle un vistazo.

¡Gracias una vez más @gimpf por los fantásticos datos! Veamos si podemos convertir eso en una decisión.

Para empezar, dividiría los algoritmos de esta manera:
Entropía rápida + buena (ordenada por velocidad):

  1. xxHash32
  2. City64 (esto probablemente será lento en x86, por lo que probablemente tendremos que elegir algo más para x86)
  3. Murmur3A

Resistente a HashDoS:

  • Marvin32
  • SipHash. Si nos inclinamos hacia esto, necesitaremos que los expertos en criptografía de Microsoft lo revisen para confirmar que los resultados de la investigación son aceptables. También tendremos que averiguar qué parámetros son lo suficientemente seguros. El documento sugiere algún lugar entre Sip-2-4 y Sip-4-8.

Fuera de la contienda (lento):

  • SpookyV2
  • Ciudad32
  • xxHash64
    * SeaHash (y no tenemos datos sobre entropía)

Fuera de contención (mala entropía):

  • Multiplicar
  • HSip

Antes de elegir un ganador, me gustaría asegurarme de que otras personas estén de acuerdo con mi clasificación anterior. Si se mantiene, creo que solo tenemos que elegir si pagar 2x por la resistencia HashDoS y luego ir por velocidad.

@morganbr Su agrupación parece estar bien. Como punto de datos en las rondas de SipHash, el proyecto Rust preguntó a Jean-Philippe Aumasson , autor de sip-hash w / DJB. Después de esa discusión, decidieron optar por sip-1-3 para tablas hash.

(Vea PR rust: # 33940 y la edición adjunta

Según los datos y los comentarios, me gustaría proponer que usemos xxHash32 en todas las arquitecturas. El siguiente paso es implementarlo. @gimpf , ¿estás interesado en armar un PR para eso?

Para aquellos preocupados por HashDoS, haré un seguimiento pronto con una propuesta para una API de hash de propósito general que debería incluir Marvin32 y puede incluir SipHash. Que también habrá un lugar apropiado para las otras implementaciones @gimpf y @tannergooding han trabajado.

@morganbr Puedo armar un PR si el tiempo lo permite. Además, personalmente también preferiría xx32, siempre que no reduzca la aceptación.

@gimpf , ¿cómo va tu tiempo? Si realmente no tienes tiempo, también podemos ver si a alguien más le gustaría intentarlo.

@morganbr Había planeado hacerlo hasta el 5 de noviembre, y todavía parece bueno que encontraré el tiempo en las próximas dos semanas.

@gimpf , suena genial. ¡Gracias por la actualización!

@terrajobst : llego un poco tarde a la fiesta (lo siento), pero ¿no podemos cambiar el tipo de devolución del método Add?

`` c #
public HashCode Agregar(Valor T);
public HashCode Agregar(Valor T, IEqualityComparercomparador);

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

Sin embargo, se puede lograr exactamente lo mismo de esta manera, aunque con una asignación de matriz menos derrochadora:

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

Tenga en cuenta que los tipos también se pueden mezclar. Obviamente, esto podría hacerse al no llamarlo con fluidez dentro de un método regular. Dado este argumento de que la interfaz fluida no es absolutamente necesaria, ¿por qué, para empezar, existe la derrochadora sobrecarga params ? Si esta sugerencia es una mala sugerencia, entonces la sobrecarga params recae en el mismo eje. Eso, y forzar un método regular para un código hash trivial pero óptimo parece una gran ceremonia.

Editar: Un implicit operator int también sería bueno para DRY, pero no exactamente crucial.

@jcdickinson

¿No podemos cambiar el tipo de retorno del método Add?

Ya lo discutimos en la propuesta anterior y fue rechazada.

¿Por qué existe la sobrecarga de parámetros derrochadores para empezar?

¿No estamos agregando sobrecargas de parámetros? Haz Ctrl + F para "parámetros" en esta página web y verás que tu comentario es el único lugar donde aparece esa palabra.

Un operador implícito int también sería bueno para DRY, pero no exactamente crucial.

Creo que eso también se discutió en algún lugar anteriormente ...

@jamesqo gracias por la explicación.

sobrecargas de parámetros

Quise decir AddRange , pero supongo que no habrá tracción en esto.

@jcdickinson AddRange estaba en la propuesta original, pero no en la versión actual. Fue rechazado por la revisión de la API (consulte https://github.com/dotnet/corefx/issues/14354#issuecomment-308190321 por @terrajobst):

Deberíamos eliminar todos los métodos AddRange porque el escenario no está claro. Es poco probable que las matrices aparezcan con mucha frecuencia. Y una vez que se involucran matrices más grandes, la pregunta es si el cálculo debe almacenarse en caché. Ver el bucle for en el lado de la llamada deja en claro que debes pensar en eso.

@gimpf Seguí adelante y rellené la propuesta con xxHash32 . Siéntase libre de aprovechar esa implementación. Tiene pruebas contra vectores xxHash32 reales.

Editar

Respecto a la interfaz. Soy plenamente consciente de que estoy haciendo una montaña con un grano de arena; siéntase libre de ignorarlo. Estoy usando la propuesta actual contra cosas reales y es una repetición muy molesta.

He estado jugando con la interfaz y ahora entiendo por qué se rechazó la interfaz fluida; es significativamente más lento.

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

Usar un método no en línea como fuente de código hash; 50 invocaciones de Add vs un método de extensión fluido:

| Método | Media | Error | StdDev | Escalado |
| ------- | ---------: | ---------: | ---------: | -------: |
| Agregar | 401,6 ns | 1.262 ns | 1.180 ns | 1,00 |
| Tally | 747,8 ns | 2.329 ns | 2.178 ns | 1,86 |

Sin embargo, el siguiente patrón funciona:

`` c #
public struct HashCode: System.Collections.IEnumerable
{
[EditorBrowsable (EditorBrowsableState.Never)]
[Obsoleto ("Este método se proporciona para la sintaxis del inicializador de colección", error: verdadero)]
public IEnumerator GetEnumerator () => lanzar una nueva NotImplementedException ();
}

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

''

También tiene características de rendimiento idénticas a la propuesta actual:

| Método | Media | Error | StdDev | Escalado |
| ------------ | ---------: | ---------: | ---------: | --- ----: |
| Agregar | 405,0 ns | 2.130 ns | 1.889 ns | 1,00 |
| Inicializador | 400,8 ns | 4.821 ns | 4.274 ns | 0,99 |

Lamentablemente, es un truco, ya que IEnumerable tiene que implementarse para mantener contento al compilador. Dicho esto, el Obsolete generará un error incluso en foreach ; realmente tendría que querer romper las cosas para encontrarse con la excepción. El MSIL en los dos es esencialmente idéntico.

@jcdickinson gracias por hacerse

Consejo profesional: una vez que acepte, GitHub lo registrará automáticamente para todas las notificaciones del repositorio (más de 500 por día), le recomendaría cambiarlo a solo "No mirando", lo que le enviará todas sus menciones y notificaciones de problemas. al que te suscribiste.

@jcdickinson , definitivamente estoy interesado en formas de evitar la repetición molesta (aunque no tengo idea de cómo se sentiría la gente acerca de la sintaxis del inicializador). Creo recordar que había dos problemas con la fluidez:

  1. El problema de rendimiento que anotaste
  2. El valor de retorno de los métodos fluidos es una copia de la estructura. Es demasiado fácil terminar perdiendo información accidentalmente haciendo cosas como:
var hc = new HashCode();
var newHc = hc.Add(foo);
hc.Add(bar);
return newHc.ToHashCode();

Dado que la propuesta en este hilo ya está aprobada (y está en camino de fusionarla), le sugiero que comience una nueva propuesta de API para cualquier cambio.

@karelz Creo que @gimpf ya tomó este problema de antemano. Como está más familiarizado con la implementación, asigne este problema a @gimpf en su lugar. ( editar: nvm)

@terrajobst Un tipo de solicitud de API de última hora para esto. Dado que marcamos GetHashCode obsoleto, implícitamente le estamos diciendo al usuario que HashCode s no son valores destinados a ser comparados, a pesar de ser estructuras que normalmente son inmutables / comparables. En ese caso, ¿deberíamos marcar Equals obsoleto también?

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

Creo que se hizo algo similar con Span .

Si eso es aceptado, entonces creo que ...

  1. Consideraría usar should not , o may not lugar de cannot en el mensaje obsoleto.
  2. Siempre que la excepción se mantenga, pondría la misma cadena en su mensaje, en caso de que el método sea llamado a través de un cast o genérico abierto.

@ Joe4evr Bien por mí; Actualicé el comentario. También puede ser beneficioso incluir el mismo mensaje en la excepción GetHashCode , entonces:

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

@morganbr ¿Por qué

El PR para exponerlo en CoreFX aún no ha pasado.

@gimpf , ¿tiene disponible el código que evaluó y / o podría ver rápidamente cómo funciona el paquete Nuget de SpookilySharp? Estoy buscando desempolvar ese proyecto después de un par de años de estancamiento y tengo curiosidad por ver cómo se mantiene.

@JonHanna Lo publicó aquí: https://github.com/gimpf/Haschisch.Kastriert

@JonHanna , me interesaría saber cómo van sus pruebas para que podamos empezar a pensar en lo que sería útil en una API de hashing no criptográfico de propósito general.

@morganbr ¿Dónde sería un foro apropiado para discutir tal API? Espero que dicha API consista en algo más que el mínimo común denominador, y tal vez una buena API también necesite un manejo mejorado de JIT wrt de estructuras más grandes. Discutiendo todo lo que podría hacerse mejor en un número separado ...

@gimpf Abrió uno para ti. dotnet / corefx # 25666

@morganbr - ¿Podemos obtener el nombre del paquete y el número de versión que incluirá esta confirmación?

@karelz , ¿podrías ayudar a @smitpatel con la información del paquete / versión?

Intentaría la compilación diaria de .NET Core; esperaría hasta mañana.
No creo que haya un paquete del que simplemente puedas depender.

Pregunta para los participantes aquí. El IDE de Roslyn permite a los usuarios generar un impl GetHashCode basado en un conjunto de campos / propiedades en su clase / estructura. Idealmente, las personas podrían usar el nuevo HashCode.Combine que se agregó en https://github.com/dotnet/corefx/pull/25013 . Sin embargo, algunos usuarios no tendrán acceso a ese código. Por lo tanto, nos gustaría poder generar un GetHashCode que funcione para ellos.

Recientemente, nos dimos cuenta de que la forma que generamos es problemática. Es decir, porque VB se compila con comprobaciones de desbordamiento de forma predeterminada, y nuestro impl causará desbordamientos. Además, VB no tiene forma de deshabilitar las comprobaciones de desbordamiento para una región de código. Está encendido o apagado por completo para toda la asamblea.

Debido a esto, me encantaría poder reemplazar el impl que proporcionamos con un formulario que no sufra estos problemas. Idealmente, el formulario generado tendría las siguientes propiedades:

  1. Una o dos líneas en GetHashCode por campo / propiedad utilizada.
  2. No se desborda.
  3. Hash razonablemente bueno. No esperamos resultados sorprendentes. Pero algo que, con suerte, ya ha sido examinado para ser decente y no tener los problemas que normalmente tienes con a + b + c + d o a ^ b ^ c ^ d .
  4. Sin dependencias / requisitos adicionales en el código.

Por ejemplo, una opción para VB sería generar algo como:

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

Pero esto depende de tener una referencia a System.ValueTuple. Idealmente, podríamos tener un impl que funciona incluso en ausencia de eso.

¿Alguien sabe acerca de un algoritmo de hash decente que pueda funcionar con estas restricciones? ¡Gracias!

-

Nota: nuestro código emitido existente es:

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

Esto claramente puede desbordarse.

Esto tampoco es un problema para C #, ya que solo podemos agregar unchecked { } alrededor de ese código. Ese control detallado no es posible en VB.

¿Alguien sabe acerca de un algoritmo de hash decente que pueda funcionar con estas restricciones? ¡Gracias!

Bueno, podrías hacer Tuple.Create(...).GetHashCode() . Obviamente, eso incurre en asignaciones, pero parece mejor que lanzar una excepción.

¿Hay alguna razón por la que no pueda simplemente decirle al usuario que instale System.ValueTuple ? Dado que es una función de lenguaje incorporada, estoy seguro de que el paquete System.ValueTuple es muy compatible con básicamente todas las plataformas, ¿verdad?

Obviamente, eso incurre en asignaciones, pero parece mejor que lanzar una excepción.

Si. Sería bueno que no causara asignaciones.

¿Hay alguna razón por la que no pueda simplemente decirle al usuario que instale System.ValueTuple?

Ese sería el comportamiento si generamos el enfoque ValueTuple. Sin embargo, nuevamente, sería bueno si pudiéramos generar algo bueno que se ajuste a la forma en que el usuario ha estructurado su código actualmente, sin hacer que cambie su estructura de una manera pesada.

Realmente parece que los usuarios de VB deberían tener una forma de abordar este problema de una manera razonable :) Pero ese enfoque me está eludiendo :)

@CyrusNajmabadi , si realmente necesita hacer su propio cálculo hash en el código del usuario, CRC32 podría funcionar ya que es una combinación de búsquedas de tablas y XOR (pero no aritmética que puede desbordarse). Sin embargo, existen algunos inconvenientes:

  1. CRC32 no tiene una gran entropía (pero probablemente sea mejor que lo que emite Roslyn ahora).
  2. Debería colocar una tabla de búsqueda de 256 entradas en algún lugar del código o emitir código para generar la tabla de búsqueda.

Si aún no lo está haciendo, espero que pueda detectar el tipo HashCode y usarlo cuando sea posible, ya que XXHash debería ser mucho mejor.

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

Hacemos lo siguiente:

  1. Utilice System.HashCode si está disponible. Hecho.
  2. De lo contrario, si está en C #:
    2a. Si no está en modo marcado: Genere hash sin enrollar.
    2b. Si está en modo marcado: Genere hash sin enrollar, envuelto en 'sin marcar {}'.
  3. De lo contrario, si está en VB:
    3b. Si no está en modo marcado: Genere hash sin enrollar.
    3c. Si está en modo marcado, pero tiene acceso a System.ValueTuple: Generar Return (a, b, c, ...).GetHashCode()
    3d. Si está en modo marcado sin acceso a System.ValueTuple. Genere hash desenrollado, pero agregue un comentario en VB que indique que es muy probable que se produzcan desbordamientos.

Es '3d' lo que es realmente desafortunado. Básicamente, alguien que use VB pero que no use ValueTuple o un sistema reciente, no podrá usarnos para obtener un algoritmo hash razonable generado para ellos.

Debería poner una tabla de búsqueda de 256 entradas en algún lugar del código

Esto sería completamente desagradable :)

¿El código de generación de tablas también es desagradable? Al menos siguiendo el ejemplo de Wikipedia , no es mucho código (pero aún tiene que ir en algún lugar de la fuente del usuario).

¿Qué tan terrible sería agregar la fuente HashCode al proyecto como lo hace Roslyn (con IL) con las definiciones de clase de atributo del compilador (mucho más simple) cuando no están disponibles a través de ningún ensamblado referenciado?

¿Qué tan terrible sería agregar la fuente HashCode al proyecto como lo hace Roslyn con las definiciones de clase de atributo del compilador (mucho más simple) cuando no están disponibles a través de ningún ensamblado referenciado?

  1. ¿La fuente HashCode no necesita un comportamiento de desbordamiento?
  2. He hojeado la fuente de HashCode. Es no trivial. Generar toda esa sustancia en el proyecto del usuario sería bastante pesado.

Me sorprende que no haya buenas formas de hacer que las matemáticas de desbordamiento funcionen en VB :(

Entonces, como mínimo, incluso si estuviéramos mezclando dos valores juntos, parece que tendríamos que crear:

`` c #
var hc1 = (uint) (valor1? .GetHashCode () ?? 0); // puede desbordar
var hc2 = (uint) (valor2? .GetHashCode () ?? 0); // puede desbordarse

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

como puede QueueRound:

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

Entonces, honestamente, no veo cómo funcionaría esto :(

Qué terrible sería agregar la fuente HashCode al proyecto como lo hace Roslyn (con IL) con (la cantidad

¿Cómo imagina que esto funcione? ¿Qué escribirían los clientes y qué harían los compiladores en respuesta?

Además, algo que abordaría todo esto es si .Net ya tiene ayudantes públicos expuestos en la API de superficie que se convierten de uint a int32 (y viceversa) sin desbordamiento.

¿Existen esos? Si es así, puedo escribir fácilmente las versiones de VB, simplemente utilizándolas para las situaciones en las que necesitamos ir entre los tipos sin desbordar.

¿El código de generación de tablas también es desagradable?

Yo creo que sí. Quiero decir, piense en esto desde la perspectiva del cliente. Solo quieren un método GetHashCode decente que sea muy autónomo y dé resultados razonables. Tener esa función y aumentar su código con basura auxiliar será bastante desagradable. También es bastante malo dado que la experiencia de C # estará bien.

Es posible que pueda obtener aproximadamente el comportamiento de desbordamiento correcto mediante la conversión desde y hacia alguna combinación de tipos de 64 bits firmados y no firmados. Algo como esto (no probado y no conozco la sintaxis de transmisión de VB):

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

¿Cómo sabes que lo siguiente no se desborda?

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

¿O el elenco final (int32) para el caso?

No me di cuenta de que usaría operaciones de conversión verificadas por desbordamiento. Supongo que podrías enmascararlo a 32 bits antes de lanzar:

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

presumiblemente 31 bits, como valor de uint32.Max también se desbordaría en la conversión a Int32 :)

Definitivamente eso es posible. Feo ... pero posible :) No habrá muchos elencos en este código.

Está bien. Creo que tengo una solución viable. El núcleo del algoritmo que generamos hoy es:

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

Digamos que estamos haciendo matemáticas de 64 bits, pero "hashCode" se ha limitado a 32 bits. Entonces <largest_32_bit> * -1521134295 + <largest_32_bit> no desbordará 64 bits. Por lo tanto, siempre podemos hacer los cálculos en 64 bits y luego limitarnos a 32 (o 32 bits) para asegurarnos de que la siguiente ronda no se desborde.

¡Gracias!

@ MaStr11 @morganbr @sharwell y todos aquí. Actualicé mi código para generar lo siguiente para 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)

¿Alguien puede comprobar mi cordura para asegurarse de que esto tiene sentido y no debería desbordarse incluso con el modo marcado activado?

@CyrusNajmabadi , eso no se desbordará (porque Int64.Max = Int32.Max * Int32.Max y sus constantes son mucho más pequeñas que eso) pero está enmascarando el bit alto a cero, por lo que es solo un hash de 31 bits. ¿Dejar el bit alto encendido se considera un desbordamiento?

@CyrusNajmabadi hashCode es un Long que puede estar entre 0 y Integer.MaxValue . ¿Por qué recibo esto?

image

Pero no, en realidad no puede desbordarse.

Por cierto, prefiero que Roslyn agregue un paquete NuGet que agregar un hash subóptimo.

pero está enmascarando el bit alto a cero, por lo que es solo un hash de 31 bits. ¿Dejar el bit alto encendido se considera un desbordamiento?

Ese es un buen punto. Creo que estaba pensando en otro algoritmo que usaba uints. Entonces, para convertir de forma segura de long a uint, necesitaba no incluir el bit de signo. Sin embargo, como todo esto es matemático firmado, creo que estaría bien enmascarar contra 0xffffffff asegurándose de que solo mantenemos los 32 bits inferiores después de agregar cada entrada.

Prefiero que Roslyn agregue un paquete NuGet que agregar un hash subóptimo.

Los usuarios ya pueden hacer eso si quieren. Se trata de qué hacer cuando los usuarios no pueden, o no pueden, agregar esas dependencias. También se trata de proporcionar un hash razonablemente "suficientemente bueno" para los usuarios. es decir, algo mejor que el enfoque común "x + y + z" que la gente suele adoptar. No se pretende que sea 'óptimo' porque no existe una buena definición de lo que es 'óptimo' cuando se trata de hash para todos los usuarios. Tenga en cuenta que el enfoque que estamos adoptando aquí es el que ya ha emitido el compilador para tipos anónimos. Muestra un comportamiento razonablemente bueno sin agregar una tonelada de complejidad al código del usuario. Con el tiempo, a medida que más y más usuarios puedan avanzar, la lata puede desaparecer lentamente y ser reemplazada por HashCode.Combine para la mayoría de las personas.

Así que trabajé un poco en eso y se me ocurrió lo siguiente que creo que aborda todas las preocupaciones:

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

La pieza que es interesante es llamar específicamente .GetHashCode() en el valor int64 producido por (hashCode * -1521134295 + a.GetHashCode()) . Llamar a .GetHashCode en este valor de 64 bits tiene dos buenas propiedades para nuestras necesidades. Primero, asegura que hashCode solo almacene un valor int32 legal en él (lo que hace que el lanzamiento final de retorno sea siempre seguro de realizar). En segundo lugar, garantiza que no perdamos información valiosa en los 32 bits superiores del valor de temperatura int64 con el que estamos trabajando.

@CyrusNajmabadi En realidad, lo que estaba preguntando es ofrecer la instalación del paquete. Me salva de tener que hacerlo.

Si escribe HashCode, si System.HashCode se proporciona en un paquete nuget de MS, Roslyn lo ofrecerá.

Quiero que genere la sobrecarga de GetHashCode inexistente e instale el paquete en la misma operación.

No creo que sea una opción adecuada para la mayoría de los usuarios. Agregar dependencias es una operación muy pesada en la que los usuarios no deben verse obligados a realizar. Los usuarios pueden decidir el momento adecuado para tomar esas decisiones y el IDE lo respetará. Ese ha sido el enfoque que hemos adoptado con todas nuestras funciones hasta ahora, y ha sido uno saludable que parece gustarle a la gente.

Nota: ¿en qué paquete nuget se incluye esta API para que agreguemos una referencia?

La implementación está en System.Private.CoreLib.dll, por lo que vendría como parte del paquete de tiempo de ejecución. El contrato es System.Runtime.dll.

Está bien. Si ese es el caso, entonces parece que un usuario obtendría esto si / cuando se cambia a un Target Framework más reciente. Ese tipo de cosas no es en absoluto un paso que tendría que hacer "generar es igual a + código hash" en el proyecto de un usuario.

¿Fue útil esta página
0 / 5 - 0 calificaciones