¿Por qué Visual Studio agrega "-1937169414" a un cálculo de código hash generado?

9

Si usa el propio menú de refactorización de Visual Studio para agregar una implementación GetHashCode a una clase como esta:

Generar menú GetHashCode

y seleccione la única propiedad int de la clase:

Pantalla de selección de miembros

genera este código en .NET Framework:

public override int GetHashCode()
{
    return -1937169414 + Value.GetHashCode();
}

( HashCode.Combine(Value)en su lugar, genera en .NET Core, que no estoy seguro si implica el mismo valor)

¿Qué tiene de especial este valor? ¿Por qué Visual Studio no usa Value.GetHashCode()directamente? Según tengo entendido, realmente no afecta la distribución de hash. Como es solo una suma, los valores consecutivos aún se acumularían juntos.

EDITAR: solo intenté esto con diferentes clases con Valuepropiedades, pero aparentemente el nombre de la propiedad afecta el número generado. Por ejemplo, si cambia el nombre de la propiedad a Halue, el número se convierte en 387336856. Gracias a Gökhan Kurt que señaló esto.

Sedat Kapanoglu
fuente
Consulte docs.microsoft.com/en-us/dotnet/api/… en la sección de comentarios. "Los códigos hash para cadenas idénticas pueden diferir entre implementaciones de .NET, entre versiones de .NET y entre plataformas de .NET (como 32 bits y 64 bits) para una única versión de .NET. En algunos casos, incluso pueden diferir por dominio de aplicación "
Enlace
@Link ¿cómo es eso relevante? eso ni siquiera es una cadena, la propiedad es un int.
Sedat Kapanoglu
[HashCode] .Combine?
Ry-
Lo sentimos enlace incorrecto: docs.microsoft.com/en-us/dotnet/api/… Este comportamiento también se aplica a Object.GetHashcode @SedatKapanoglu
Enlace
2
-1937169414es la multiplicación entera de -1521134295y -783812246. El número más significativo aquí es el -1521134295que aparece en cada cálculo de código hash. -783812246es el número de semilla Se elige un número de semilla en función del número de miembros en la ecuación. En las clases anónimas, el número de semilla se calcula en función de los nombres de campo. Por lo tanto, hay tantos números de semillas como números enteros. Podemos suponer que un número de semilla es aleatorio. En cuanto a la importancia de -1521134295, creo que reduce la colisión y solo un desarrollador interno podría responder con precisión cómo.
Gökhan Kurt

Respuestas:

2

Si busca -1521134295en los repositorios de Microsoft, verá que aparece varias veces

La mayoría de los resultados de búsqueda están en las GetHashCodefunciones, pero todos tienen el siguiente formulario

int hashCode = SOME_CONSTANT;
hashCode = hashCode * -1521134295 + field1.GetHashCode();
hashCode = hashCode * -1521134295 + field2.GetHashCode();
// ...
return hashCode;

El primero hashCode * -1521134295 = SOME_CONSTANT * -1521134295se multiplicará previamente durante el tiempo de generación por el generador o durante el tiempo de compilación por CSC. Esa es la razón -1937169414en tu código

Profundizar en los resultados revela la parte de generación de código que se puede encontrar en la función CreateGetHashCodeMethodStatements

const int hashFactor = -1521134295;

var initHash = 0;
var baseHashCode = GetBaseGetHashCodeMethod(containingType);
if (baseHashCode != null)
{
    initHash = initHash * hashFactor + Hash.GetFNVHashCode(baseHashCode.Name);
}

foreach (var symbol in members)
{
    initHash = initHash * hashFactor + Hash.GetFNVHashCode(symbol.Name);
}

Como puede ver, el hash depende de los nombres de los símbolos. En esa función también se llama la constante permuteValue, probablemente porque después de la multiplicación los bits se permutan de alguna manera

// -1521134295
var permuteValue = CreateLiteralExpression(factory, hashFactor);

Hay algunos patrones si vemos el valor en binario: 101001 010101010101010 101001 01001 o 10100 1010101010101010 10100 10100 1. Pero si multiplicamos un valor arbitrario con eso, entonces hay muchos traslapes superpuestos, por lo que no podría ver cómo funciona. La salida también puede tener un número diferente de bits establecidos, por lo que no es realmente una permutación

Puede encontrar otro generador en AnonymousTypeGetHashCodeMethodSymbol de Roslyn que llama a la constanteHASH_FACTOR

//  Method body:
//
//  HASH_FACTOR = 0xa5555529;
//  INIT_HASH = (...((0 * HASH_FACTOR) + GetFNVHashCode(backingFld_1.Name)) * HASH_FACTOR
//                                     + GetFNVHashCode(backingFld_2.Name)) * HASH_FACTOR
//                                     + ...
//                                     + GetFNVHashCode(backingFld_N.Name)

La verdadera razón para elegir ese valor aún no está clara

phuclv
fuente
Esta es una gran investigación, gracias. No sabía que la generación de código hash estaba en Roslyn, pensé que sería Visual Studio en sí.
Sedat Kapanoglu
3

Como GökhanKurt explicó en los comentarios, el número cambia según los nombres de propiedad involucrados. Si cambia el nombre de la propiedad a Halue, el número se convierte en 387336856 en su lugar. Lo había intentado con diferentes clases pero no pensé en renombrar la propiedad.

El comentario de Gökhan me hizo comprender su propósito. Está compensando valores hash basados ​​en un desplazamiento determinista, pero distribuido aleatoriamente. De esta forma, la combinación de valores hash para diferentes clases, incluso con una simple adición, sigue siendo ligeramente resistente a las colisiones hash.

Por ejemplo, si tiene dos clases con implementaciones de GetHashCode similares:

public class A
{
    public int Value { get; set;}
    public int GetHashCode() => Value;
}

public class B
{
    public int Value { get; set;}
    public override int GetHashCode() => Value;
}

y si tienes otra clase que contiene referencias a estos dos:

public class C
{
    public A ValueA { get; set; }
    public B ValueB { get; set; }
    public override int GetHashCode()
    {
        return ValueA.GetHashCode() + ValueB.GetHashCode();
    }
}

una combinación pobre como esta sería propensa a colisiones hash porque el código hash resultante se acumularía alrededor de la misma área para diferentes valores de ValueA y ValueB si sus valores están cerca uno del otro. Realmente no importa si usa la multiplicación o las operaciones bit a bit para combinarlas, seguirían siendo propensas a colisiones sin un desplazamiento uniformemente distanciado. Como muchos valores enteros utilizados en la programación se acumulan alrededor de 0, tiene sentido utilizar dicho desplazamiento

Aparentemente, es una buena práctica tener un desplazamiento aleatorio con buenos patrones de bits.

Todavía no estoy seguro de por qué no usan compensaciones completamente aleatorias, probablemente para no romper ningún código que se base en el determinismo de GetHashCode (), pero sería genial recibir un comentario del equipo de Visual Studio sobre esto.

Sedat Kapanoglu
fuente