Por que o Visual Studio adiciona "-1937169414" a um cálculo de código de hash gerado?

9

Se você usar o próprio menu de refatoração do Visual Studio para adicionar uma implementação GetHashCode a uma classe como esta:

Menu Gerar GetHashCode

e selecione a única propriedade int na classe:

Tela de seleção de membros

ele gera esse código no .NET Framework:

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

(gera HashCode.Combine(Value)no .NET Core, o que não tenho certeza se envolve o mesmo valor)

O que há de especial nesse valor? Por que o Visual Studio não usa Value.GetHashCode()diretamente? Pelo que entendi, isso realmente não afeta a distribuição de hash. Como é apenas uma adição, valores consecutivos ainda se acumulam juntos.

Edição: Eu só tentei isso com diferentes classes com Valuepropriedades, mas aparentemente o nome da propriedade afeta o número gerado. Por exemplo, se você renomear a propriedade como Halue, o número se tornará 387336856. Agradecimentos a Gökhan Kurt, que apontou isso.

Sedat Kapanoglu
fonte
Consulte docs.microsoft.com/en-us/dotnet/api/… na seção de comentários. "Os códigos hash para seqüências de caracteres idênticas podem diferir nas implementações do .NET, nas versões do .NET e nas plataformas do .NET (como 32 bits e 64 bits) para uma única versão do .NET. Em alguns casos, eles podem até diferir pelo domínio do aplicativo "
Link
@Link como isso é relevante? isso nem é uma string, a propriedade é uma int.
Sedat Kapanoglu
[HashCode] .Combine?
Ry-
Desculpe, link incorreto: docs.microsoft.com/en-us/dotnet/api/… Esse comportamento também se aplica ao Object.GetHashcode @SedatKapanoglu
Link
2
-1937169414é a multiplicação inteira de -1521134295e -783812246. O número mais significativo aqui é o -1521134295que aparece em todo cálculo de código de hash. -783812246é o número da semente. Um número de semente é escolhido com base no número de membros na equação. Em classes anônimas, o número da semente é calculado com base nos nomes dos campos. Portanto, existem tantos números de sementes quanto números inteiros. Podemos assumir que um número inicial é aleatório. Quanto ao significado de -1521134295, acho que reduz a colisão e apenas um desenvolvedor interno seria capaz de responder com precisão como.
Gökhan Kurt

Respostas:

2

Se você procurar -1521134295nos repositórios da Microsoft, verá que ele aparece várias vezes

A maioria dos resultados da pesquisa está nas GetHashCodefunções, mas todas elas têm o seguinte formato

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

O primeiro hashCode * -1521134295 = SOME_CONSTANT * -1521134295será pré-multiplicado durante o tempo de geração pelo gerador ou durante o tempo de compilação pelo CSC. Essa é a razão para -1937169414no seu código

Aprofundar nos resultados revela a parte da geração de código que pode ser encontrada na função 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 você pode ver, o hash depende dos nomes dos símbolos. Nessa função, a constante também é chamada permuteValue, provavelmente porque após a multiplicação os bits são permutados de alguma maneira

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

Existem alguns padrões se visualizarmos o valor em binário: 101001 010101010101010 101001 01001ou 10100 1010101010101010 10100 10100 1. Mas se multiplicarmos um valor arbitrário por isso, haverá muitas cargas sobrepostas, então não pude ver como funciona. A saída também pode ter um número diferente de bits definidos, portanto, não é realmente uma permutação

Você pode encontrar outro gerador no AnonymousTypeGetHashCodeMethodSymbol de Roslyn, que chama a 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)

A verdadeira razão para escolher esse valor ainda não está clara

phuclv
fonte
Esta é uma ótima pesquisa, obrigado. Eu não sabia que a geração de código hash estava em Roslyn, pensei que seria o próprio Visual Studio.
Sedat Kapanoglu
3

Como GökhanKurt explicou nos comentários, o número muda com base nos nomes das propriedades envolvidas. Se você renomear a propriedade como Halue, o número se tornará 387336856. Eu tentei com classes diferentes, mas não pensei em renomear a propriedade.

O comentário de Gökhan me fez entender seu propósito. Ele está compensando valores de hash com base em um deslocamento determinístico, mas distribuído aleatoriamente. Dessa forma, a combinação de valores de hash para diferentes classes, mesmo com uma simples adição, ainda é um pouco resistente a colisões de hash.

Por exemplo, se você tiver duas classes com implementações GetHashCode semelhantes:

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

e se você tiver outra classe que contenha referências a esses dois:

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

uma combinação ruim como essa seria propensa a colisões de hash, porque o código de hash resultante se acumularia na mesma área para valores diferentes de ValueA e ValueB se os valores deles estivessem próximos. Realmente não importa se você usa operações de multiplicação ou bit a bit para combiná-las, elas ainda estarão sujeitas a colisões sem um deslocamento uniformemente distanciado. Como muitos valores inteiros usados ​​na programação são acumulados em torno de 0, faz sentido usar esse deslocamento

Aparentemente, é uma boa prática ter um deslocamento aleatório com bons padrões de bits.

Ainda não sei por que eles não usam deslocamentos completamente aleatórios, provavelmente para não quebrar nenhum código que dependa do determinismo de GetHashCode (), mas seria ótimo receber um comentário da equipe do Visual Studio sobre isso.

Sedat Kapanoglu
fonte