Runtime: Proposta: adicione System.HashCode para facilitar a geração de bons códigos hash.

Criado em 9 dez. 2016  ·  182Comentários  ·  Fonte: dotnet/runtime

Atualização 16/06/17: Procurando voluntários

O formato da API foi finalizado. No entanto, ainda estamos decidindo sobre o melhor algoritmo de hash de uma lista de candidatos a serem usados ​​para a implementação e precisamos de alguém para nos ajudar a medir a taxa de transferência / distribuição de cada algoritmo. Se você gostaria de assumir essa função, deixe um comentário abaixo e @karelz irá atribuir esse problema a você.

Atualização 13/06/17: Proposta aceita!

Aqui está a API que foi aprovada por @terrajobst em 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();
    }
}

Segue o texto original desta proposta.

Justificativa

A geração de um bom código hash não deve exigir o uso de constantes mágicas feias e pequenas alterações em nosso código. Deve ser menos tentador escrever uma implementação GetHashCode concisa, mas ruim, como

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

Proposta

Devemos adicionar um tipo HashCode para encobrir a criação do código hash e evitar forçar os desenvolvedores a se confundir nos detalhes confusos. Aqui está minha proposta, que é baseada em https://github.com/dotnet/corefx/issues/14354#issuecomment -305019329, com algumas pequenas revisões.

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

Observações

Veja @terrajobst 's comentário na https://github.com/dotnet/corefx/issues/14354#issuecomment -305019329 para os objectivos desta API; todas as suas observações são válidas. Gostaria de destacar estes em particular, no entanto:

  • A API não precisa produzir um hash criptográfico forte
  • A API fornecerá "um" código hash, mas não garante um algoritmo de código hash específico. Isso nos permite usar um algoritmo diferente posteriormente ou usar algoritmos diferentes em arquiteturas diferentes.
  • A API garantirá que dentro de um determinado processo os mesmos valores produzirão o mesmo código hash. Diferentes instâncias do mesmo aplicativo provavelmente produzirão códigos hash diferentes devido à randomização. Isso nos permite garantir que os consumidores não possam persistir com os valores de hash e, acidentalmente, confiar que eles sejam estáveis ​​em todas as execuções (ou pior, nas versões da plataforma).
api-approved area-System.Numerics up-for-grabs

Comentários muito úteis

Decisões

  • Devemos remover todos os métodos AddRange porque o cenário não está claro. É pouco provável que os arrays apareçam com muita frequência. E uma vez que matrizes maiores estão envolvidas, a questão é se a computação deve ser armazenada em cache. Ver o loop for do lado da chamada deixa claro que você precisa pensar sobre isso.
  • Também não queremos adicionar sobrecargas de IEnumerable a AddRange porque elas seriam alocadas.
  • Não achamos que precisamos da sobrecarga para Add que leva string e StringComparison . Sim, eles são provavelmente mais eficientes do que chamar por meio de IEqualityComparer , mas podemos consertar isso mais tarde.
  • Achamos que marcar GetHashCode como obsoleto com erro é uma boa ideia, mas iríamos um passo adiante e também nos esconderíamos do IntelliSense.

Isso nos deixa com:

`` `C #
// Vai morar no conjunto central
// .NET Framework: mscorlib
// .NET Core: System.Runtime / System.Private.CoreLib
sistema de namespace
{
public struct HashCode
{
public static int Combine(T1 valor1);
public static int Combine(T1 valor1, T2 valor2);
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 comentários

Proposta: adicionar suporte para randomização de hash

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

T ou Type type é necessário para obter o mesmo hash aleatório para o mesmo tipo.

Proposta: adicionar suporte para coleções

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

Acho que não há necessidade de sobrecargas Combine(_field1, _field2, _field3, _field4, _field5) porque o próximo código HashCode.Empty.Combine(_field1).Combine(_field2).Combine(_field3).Combine(_field4).Combine(_field5); deve ser otimizado em linha sem chamadas Combine.

@AlexRadch

Proposta: adicionar suporte para coleções

Sim, isso fazia parte do meu plano final para esta proposta. No entanto, acho importante nos concentrarmos em como queremos que a API se pareça antes de adicionarmos esses métodos.

Ele queria usar um algoritmo diferente, como o hash Marvin32 que é usado para strings no coreclr. Isso exigiria expandir o tamanho do HashCode para 8 bytes.

Que tal ter os tipos Hash32 e Hash64 que armazenariam internamente 4 ou 8 bytes de dados? Documente os prós / contras de cada um. Hash64 é bom para o X, mas é potencialmente mais lento. Hash32 sendo mais rápido, mas potencialmente não tão distribuído (ou qualquer que seja a compensação).

Ele queria randomizar a semente de hash, então os hashes não seriam determinísticos.

Este parece ser um comportamento útil. Mas eu podia ver as pessoas querendo controlar isso. Portanto, talvez deva haver duas maneiras de criar o Hash, uma que não leva nenhuma semente (e usa uma semente aleatória) e outra que permite que a semente seja fornecida.

Nota: Roslyn adoraria se isso pudesse ser fornecido no Fx. Estamos adicionando um recurso para cuspir um GetHashCode para o usuário. Atualmente, ele gera 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 não é uma grande experiência e expõe muitos conceitos feios. Ficaríamos entusiasmados em ter uma API Hash.Whatever que poderíamos chamar em seu lugar.

Obrigado!

E sobre MurmurHash? É razoavelmente rápido e tem propriedades de hash muito boas. Também há duas implementações diferentes, uma que gera hashes de 32 bits e outra que gera hashes de 128 bits.

Também há implementações vetorizadas para os formatos de 32 bits e 128 bits.

@tannergooding MurmurHash é rápido, mas não seguro, pelo que parece esta postagem do blog .

@jkotas , houve algum trabalho no JIT em torno da geração de código melhor para estruturas> 4 bytes em 32 bits desde nossas discussões no ano passado? Além disso, o que você acha da proposta de

Que tal ter os tipos Hash32 e Hash64 que armazenariam internamente 4 ou 8 bytes de dados? Documente os prós / contras de cada um. Hash64 é bom para o X, mas é potencialmente mais lento. Hash32 sendo mais rápido, mas potencialmente não tão distribuído (ou qualquer que seja a compensação).

Ainda acho que esse tipo seria muito valioso para oferecer aos desenvolvedores e seria ótimo tê-lo na versão 2.0.

@jamesqo , não acho que esta implementação precise ser criptograficamente segura (esse é o propósito das funções de hash criptograficamente explícitas).

Além disso, esse artigo se aplica a Murmur2. O problema foi resolvido no algoritmo Murmur3.

o JIT em torno da geração de código melhor para estruturas> 4 bytes em 32 bits desde nossas discussões no ano passado

Eu não estou ciente de nenhum.

o que você acha da proposta de

Os tipos de estrutura devem ser escolhas simples que funcionam bem para 95% + dos casos. Eles podem não ser os mais rápidos, mas tudo bem. Ter que escolher entre Hash32 e Hash64 não é uma escolha simples.

Por mim tudo bem. Mas podemos pelo menos ter uma solução boa o suficiente para esses casos de 95%? No momento não há nada ...: - /

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

@CyrusNajmabadi Por que você está chamando EqualityComparer aqui, e não apenas this.s.GetHashCode ()?

Para não structs: para que não precisemos verificar se há nulos.

Isso é próximo ao que geramos para tipos anônimos nos bastidores também. Eu otimizo o caso de valores não nulos conhecidos para gerar um código que seria mais agradável para os usuários. Mas seria bom ter apenas uma API integrada para isso.

A chamada para EqualityComparer.Default.GetHashCode é 10x + mais cara do que verificar se há nulo ....

A chamada para EqualityComparer.Default.GetHashCode é 10x + mais cara do que verificar se há nulo.

Parece um problema. se houvesse uma boa API de código hash, poderíamos chamar o Fx para o qual eu poderia adiar :)

(também, temos esse problema em nossos tipos anônimos, pois é isso que geramos lá também).

Não tenho certeza do que fazemos com as tuplas, mas acho que é semelhante.

Não tenho certeza do que fazemos com as tuplas, mas acho que é semelhante.

System.Tuple passa por EqualityComparer<Object>.Default por razões históricas. System.ValueTuple chama Object.GetHashCode com verificação nula - https://github.com/dotnet/coreclr/blob/master/src/mscorlib/shared/System/ValueTuple.cs#L809.

Oh não. Parece que a tupla pode apenas usar "HashHelpers". Isso poderia ser exposto para que os usuários possam obter o mesmo benefício?

Excelente. Estou feliz em fazer algo semelhante. Comecei com nossos tipos anônimos porque imaginei que fossem as melhores práticas razoáveis. Se não, tudo bem. :)

Mas não é por isso que estou aqui. Estou aqui para obter algum sistema que realmente combine os hashes de forma eficaz. Se / quando isso puder ser fornecido, teremos o prazer de passar a chamar isso em vez de codificar em números aleatórios e combinar valores de hash.

Qual seria a forma de API que você acha que funcionaria melhor para o código gerado pelo compilador?

Literalmente, qualquer uma das soluções de 32 bits que foram apresentadas anteriormente estaria bem para mim. Caramba, soluções de 64 bits estão bem para mim. Apenas algum tipo de API que você pode obter que diz "posso combinar hashes de alguma forma razoável e produzir um resultado razoavelmente distribuído".

Não consigo conciliar essas afirmações:

Tínhamos uma estrutura HashCode imutável com 4 bytes de tamanho. Ele tinha um método Combine (int), que misturava o código hash fornecido com seu próprio código hash por meio de um algoritmo do tipo DJBX33X e retornava um novo HashCode.

@jkotas não achou que o algoritmo do tipo DJBX33X fosse robusto o suficiente.

E

Os tipos de estrutura devem ser escolhas simples que funcionam bem para 95% + dos casos.

Não podemos criar um hash simples de acumulação de 32 bits que funcione bem o suficiente para 95% dos casos? Quais são os casos que não são bem tratados aqui e por que achamos que estão no caso de 95%?

@jkotas , o desempenho é realmente crítico para esse tipo? Acho que, em média, coisas como pesquisas de hashtable e isso levariam muito mais tempo do que algumas cópias de estrutura. Se acabar sendo um gargalo, seria razoável pedir à equipe JIT para otimizar cópias de struct de 32 bits depois que a API for lançada para que eles tenham algum incentivo, em vez de bloquear essa API quando ninguém estiver trabalhando na otimização cópias?

Não podemos criar um hash simples de acumulação de 32 bits que funcione bem o suficiente para 95% dos casos?

Fomos muito queimados por padrão de 32 bits acumulando hash para strings, e é por isso que Marvin hash para strings no .NET Core - https://github.com/dotnet/corert/blob/87e58839d6629b5f90777f886a2f52d7a99c076f/src/System.Private.CoreLib/ src / System / Marvin.cs # L25. Acho que não queremos repetir o mesmo erro aqui.

@jkotas , o desempenho é realmente crítico para esse tipo?

Não acho que o desempenho seja crítico. Uma vez que parece que esta API será usada pelo código do compilador gerado automaticamente, acho que devemos preferir um código gerado menor em vez de sua aparência. O padrão não fluente é um código menor.

Fomos muito queimados por padrão 32 bits acumulando hash para string

Isso não parece ser o caso de 95%. Estamos falando de desenvolvedores normais que querem apenas um hash "bom o suficiente" para todos aqueles tipos em que fazem as coisas manualmente hoje.

Uma vez que parece que esta API será usada pelo código do compilador gerado automaticamente, acho que devemos preferir um código gerado menor em vez de sua aparência. O padrão não fluente é um código menor.

Não deve ser usado pelo compilador Roslyn. Deve ser usado pelo Roslyn IDE quando ajudamos os usuários a gerar GetHashCodes para seus tipos. Este é o código que o usuário verá e terá que manter, e ter algo sensato como:

`` `c #
retornar 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;

Quer dizer, já temos este código no Fx:

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

Achamos que é bom o suficiente para tuplas. Não está claro para mim por que seria um problema torná-lo disponível para usuários que o desejam para seus próprios tipos.

Observação: até mesmo consideramos fazer isso em Roslyn:

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

Mas agora você está forçando as pessoas a gerar uma estrutura (potencialmente grande) apenas para obter algum tipo de comportamento de hash padrão razoável.

Estamos falando de desenvolvedores normais que querem apenas um hash "bom o suficiente" para todos aqueles tipos em que fazem as coisas manualmente hoje.

O hash da string original era um hash "bom o suficiente" que funcionava bem para desenvolvedores normais. Mas então foi descoberto que os servidores da web ASP.NET eram vulneráveis ​​a ataques DoS porque eles tendem a armazenar o material recebido em hashtables. Portanto, o hash "bom o suficiente" basicamente se transformou em um problema de segurança ruim.

Achamos que é bom o suficiente para tuplas

Não necessariamente. Fizemos uma medida de back stop para tuplas para tornar o hashcode randomizado que nos dá a opção de modificar o algoritmo mais tarde.

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

Isso parece razoável para mim.

Eu não entendi sua posição. Você parece estar dizendo duas coisas:

O hash da string original era um hash "bom o suficiente" que funcionava bem para desenvolvedores normais. Mas então foi descoberto que os servidores da web ASP.NET eram vulneráveis ​​a ataques DoS porque eles tendem a armazenar o material recebido em hashtables. Portanto, o hash "bom o suficiente" basicamente se transformou em um problema de segurança ruim.

Ok, se for esse o caso, vamos fornecer um código hash que seja bom para pessoas que têm preocupações com segurança / DoS.

Os tipos de estrutura devem ser escolhas simples que funcionam bem para 95% + dos casos.

Ok, se for esse o caso, vamos fornecer um código hash que seja bom o suficiente para 95% dos casos. As pessoas que têm preocupações com segurança / DoS podem usar os formulários especializados documentados para esse fim.

Não necessariamente. Fizemos uma medida de back stop para tuplas para tornar o hashcode randomizado que nos dá a opção de modificar o algoritmo mais tarde.

OK. Podemos expor isso para que os usuários possam usar esse mesmo mecanismo.

-
Estou realmente lutando aqui porque parece que estamos dizendo "porque não podemos fazer uma solução universal, cada um tem que fazer a sua própria". Esse parece ser um dos piores lugares para se estar. Porque certamente a maioria de nossos clientes não está pensando em lançar seu próprio 'marvin hash' para questões de DoS. Eles estão apenas adicionando, xorando ou combinando de outra forma hashes de campo em um hash final.

Se nos preocupamos com o caso de 95%, devemos apenas fazer um hash geralmente bom. SE nos preocupamos com o caso de 5%, podemos fornecer uma solução especializada para isso.

Isso parece razoável para mim.

Ótimo :) Podemos então expor:

`` `c #
namespace System.Numerics.Hashing
{
classe estática interna HashHelpers
{
public static readonly int RandomSeed = new Random (). Next (Int32.MinValue, Int32.MaxValue);

    public static int Combine(int h1, int h2)
    {
        // RyuJIT optimizes this to use the ROL instruction
        // Related GitHub pull request: dotnet/coreclr#1830
        uint rol5 = ((uint)h1 << 5) | ((uint)h1 >> 27);
        return ((int)rol5 + h1) ^ h2;
    }
}
Roslyn could then generate:

```c#
     return Hash.Combine(Hash.RandomSeed,
                         this.A?.GetHashCode() ?? 0,
                         this.B?.GetHashCode() ?? 0,
                         this.C?.GetHashCode() ?? 0);

Isso teria o benefício de realmente ser "bom o suficiente" para a grande maioria dos casos, ao mesmo tempo que conduz as pessoas ao bom caminho da inicialização com valores aleatórios para que não dependam de hashes não aleatórios.

As pessoas que têm preocupações com segurança / DoS podem usar os formulários especializados documentados para esse fim.

Todo aplicativo ASP.NET tem preocupação com segurança / DoS.

Ótimo :) Podemos então expor:

Isso é diferente do que eu disse que é razoável.

O que você acha sobre https://github.com/aspnet/Common/blob/dev/shared/Microsoft.Extensions.HashCodeCombiner.Sources/HashCodeCombiner.cs . É o que é usado internamente no ASP.NET em vários lugares hoje, e é com o que eu ficaria muito feliz (exceto que a função de combinação precisa ser mais forte - detalhes de implementação que podemos continuar ajustando).

@jkotas Ouvi dizer que: p

Portanto, o problema aqui é que os desenvolvedores não sabem quando estão suscetíveis a ataques DoS, porque não é algo que eles pensam sobre isso, e é por isso que trocamos as strings para usar o Marvin32.

Não devemos cair no caminho de dizer "95% dos casos não importam", porque não temos como provar isso, e devemos errar por excesso de cautela mesmo quando isso tem um custo de desempenho. Se você vai se afastar disso, a implementação do código hash precisa de uma revisão do Crypto Board, não apenas nós decidindo "Isso parece bom o suficiente".

Todo aplicativo ASP.NET tem preocupação com segurança / DoS.

OK. Então, como você está lidando com o problema hoje em que ninguém tem ajuda com códigos hash e, portanto, provavelmente está fazendo as coisas mal? É claro que é aceitável ter esse estado do mundo. Então, o que é prejudicado por fornecer um sistema de hash razoável que provavelmente tem um desempenho melhor do que o que as pessoas estão usando atualmente?

porque não temos como provar isso e devemos errar por excesso de cautela, mesmo quando há um custo de desempenho

Se você não fornecer algo, as pessoas continuarão a fazer as coisas mal. A rejeição do "bom o suficiente" porque não há nada perfeito significa apenas o pobre status quo que temos hoje.

Todo aplicativo ASP.NET tem preocupação com segurança / DoS.

Você pode explicar isso? Pelo que entendi, você tem uma preocupação de DoS se estiver aceitando entradas arbitrárias e, em seguida, armazenando-as em alguma estrutura de dados que funciona mal se as entradas puderem ser especialmente criadas. Ok, eu entendo como isso é uma preocupação com as strings que alguém obtém em cenários da web que vêm do usuário.

Então, como isso se aplica ao restante dos tipos que não estão sendo usados ​​neste cenário?

Temos estes conjuntos de tipos:

  1. Tipos de usuários que precisam ser protegidos contra DoS. No momento, não fornecemos nada para ajudar, então estamos em uma posição ruim, pois as pessoas provavelmente não estão fazendo a coisa certa.
  2. Tipos de usuários que não precisam ser protegidos contra DoS. No momento, não fornecemos nada para ajudar, então estamos em uma situação ruim, pois as pessoas provavelmente não estão fazendo a coisa certa.
  3. Tipos de framework que precisam ser protegidos contra DoS. No momento, nós os tornamos seguros contra DoS, mas por meio de APIs não os expomos.
  4. Tipos de estrutura que não precisam ser seguros contra DoS. No momento, fornecemos hashes, mas por meio de APIs não os expomos.

Basicamente, achamos que esses casos são importantes, mas não o suficiente para fornecer uma solução aos usuários para lidar com '1' ou '2'. Porque estamos preocupados que uma solução para '2' não seja boa para '1', nem mesmo iremos fornecê-la em primeiro lugar. E se não estamos dispostos a sequer fornecer uma solução para '1', parece que estamos em uma posição incrivelmente estranha. Estamos preocupados com DoSing e ASP, mas não muito preocupados em realmente ajudar as pessoas. E porque não ajudaremos as pessoas com isso, nem mesmo estamos dispostos a ajudar nos casos não DoS.

-

Se esses dois casos são importantes (que estou disposto a aceitar), por que não fornecer apenas duas APIs? Documente-os. Deixe claro para que servem. Se as pessoas os usarem corretamente, ótimo . Se as pessoas não os usarem corretamente, ainda está bem. Afinal, eles provavelmente não está fazendo as coisas corretamente hoje de qualquer maneira, por isso, como estão as coisas pior?

O que você pensa sobre

Não tenho opinião, de uma forma ou de outra. Se for uma API que os clientes possam usar, com desempenho aceitável e que forneça uma API simples com código claro, então acho que está tudo bem.

Acho que seria bom ter um formulário estático simples que lida com o caso de 99% de querer combinar um conjunto de campos / propriedades de uma forma ordenada. Parece que tal coisa pode ser adicionada a este tipo de forma bastante simples.

Acho que seria bom ter uma forma estática simples

Aceita.

Acho que seria bom ter um formulário estático simples que lida com o caso de 99% de querer combinar um conjunto de campos / propriedades de uma forma ordenada. Parece que tal coisa pode ser adicionada a este tipo de forma bastante simples.

Aceita.

Estou disposto a conhecê-los no meio do caminho, porque realmente quero ver algum tipo de API surgindo. @jkotas Eu ainda não entendo que você se oponha a adicionar uma API baseada em instância imutável; primeiro você disse que era porque as cópias de 32 bits seriam lentas, depois porque a API mutável seria mais concisa (o que não é verdade; h.Combine(a).Combine(b) (versão imutável) é menor que h.Combine(a); h.Combine(b); (mutável versão)).

Dito isso, estou disposto a voltar para:

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
}

Isso parece razoável?

Não posso editar minha postagem agora, mas percebi que nem todos os métodos podem aceitar T. Nesse caso, podemos apenas ter 8 sobrecargas aceitando todos os ints e forçar o usuário a chamar GetHashCode.

Se esses dois casos são importantes (que estou disposto a aceitar), por que não fornecer apenas duas APIs? Documente-os. Deixe claro para que servem. Se as pessoas os usarem corretamente, ótimo. Se as pessoas não os usarem corretamente, ainda está bem. Afinal, eles provavelmente não estão fazendo as coisas corretamente hoje, então como as coisas estão piores?

Porque as pessoas não usam as coisas corretamente quando estão lá. Vamos dar um exemplo simples, XSS. Desde o início, até mesmo os formulários da web tinham a capacidade de codificar a saída em HTML. No entanto, os desenvolvedores não sabiam do risco, não sabiam como fazer isso corretamente e só descobriram quando era tarde demais, seu aplicativo foi publicado e, opa, agora seu cookie de autenticação foi removido.

Dar às pessoas uma escolha de segurança pressupõe que elas

  1. Saiba sobre o problema.
  2. Entenda quais são os riscos.
  3. Pode avaliar esses riscos.
  4. Pode descobrir facilmente a coisa certa a fazer.

Essas suposições geralmente não são válidas para a maioria dos desenvolvedores, eles só descobrem o problema quando é tarde demais. Os desenvolvedores não vão a conferências de segurança, não leem white papers e não entendem as soluções. Portanto, no cenário ASP.NET HashDoS, escolhemos por eles, os protegemos por padrão, porque era a coisa certa a se fazer e tinha o maior impacto. No entanto, só o aplicamos a strings, e isso deixou as pessoas que estavam construindo classes personalizadas a partir da entrada do usuário em um lugar ruim. Devemos fazer a coisa certa e ajudar a proteger esses clientes agora, e torná-los o padrão, tendo um poço de sucesso, não de fracasso. O design da API para segurança às vezes não é uma questão de escolha, mas sim de ajudar o usuário, quer ele saiba disso ou não.

Um usuário sempre pode criar um hash não focado na segurança; então, dadas as duas opções

  1. O utilitário hash padrão não reconhece a segurança; o usuário pode criar uma função hash com reconhecimento de segurança
  2. O utilitário hash padrão está ciente da segurança; o usuário pode criar uma função hash personalizada não relacionada à segurança

Então o segundo é provavelmente melhor; e o que é sugerido não teria o impacto de desempenho de um hash criptográfico completo; então é um bom compromisso?

Uma das questões em execução nesses threads é qual algoritmo é perfeito para todos. Acho que é seguro dizer que não existe um único algoritmo perfeito. No entanto, não acho que isso deve nos impedir de fornecer algo melhor do que um código como o que @CyrusNajmabadi mostrou, que tende a ter entropia pobre para entradas .NET comuns, bem como outros bugs de hasher comuns (como perda de dados de entrada ou ser facilmente reajustável).

Eu gostaria de propor algumas opções para contornar o problema do "melhor algoritmo":

  1. Opções explícitas: estou planejando enviar uma proposta de API em breve para um conjunto de hashes não criptográficos (talvez xxHash, Marvin32 e SpookyHash, por exemplo). Essa API tem um uso ligeiramente diferente de um tipo HashCode ou HashCodeHelper, mas para fins de discussão, suponha que possamos resolver essas diferenças. Se usarmos essa API para GetHashCode:

    • O código gerado é explícito sobre o que está fazendo - se Roslyn gerar Marvin32.Create(); , ele permite que usuários avançados saibam o que decidiu fazer e eles podem facilmente alterá-lo para outro algoritmo no pacote, se quiserem.

    • Isso significa que não precisamos nos preocupar em interromper as alterações. Se começarmos com um algoritmo lento / entropia pobre / não aleatório, podemos simplesmente atualizar o Roslyn para começar a gerar outra coisa no novo código. O código antigo continuará usando o hash antigo e o novo código usará o novo hash. Os desenvolvedores (ou uma correção de código Roslyn) podem alterar o código antigo se quiserem.

    • A maior desvantagem em que posso pensar é que algumas das otimizações que podemos desejar para GetHashCode podem ser prejudiciais para outros algoritmos. Por exemplo, enquanto um estado interno de 32 bits funciona bem com estruturas imutáveis, um estado interno de 256 bits em (digamos) CityHash pode perder muito tempo copiando.

  1. Randomização: Comece com um algoritmo apropriadamente randomizado (o código @CyrusNajmabadi mostrado com um valor inicial aleatório não conta, pois é provável que seja possível eliminar a aleatoriedade). Isso garante que podemos alterar a implementação sem problemas de compatibilidade. Ainda precisaríamos ser muito sensíveis sobre as mudanças de desempenho se mudarmos o algoritmo. No entanto, isso também seria uma vantagem potencial, pois poderíamos fazer escolhas por arquitetura (ou mesmo por dispositivo). Por exemplo, este site mostra que xxHash é mais rápido em um Mac x64, enquanto SpookyHash é mais rápido em Xbox e iPhone. Se seguirmos esse caminho com a intenção de alterar algoritmos em algum ponto, talvez precisemos pensar em projetar uma API que ainda tenha um desempenho razoável se houver um estado interno de 64 bits ou mais.

CC @bartonjs , @terrajobst

@morganbr Não existe um único algoritmo perfeito, mas acho que ter algum algoritmo, que funciona razoavelmente bem na maioria das vezes, exposto usando uma API simples e fácil de entender, é a coisa mais útil que pode ser feita. Ter um conjunto de algoritmos além disso, para usos avançados, é bom. Mas não deveria ser a única opção, eu não deveria ter que aprender quem é Marvin apenas para poder colocar meus objetos em um Dictionary .

Eu não deveria ter que aprender quem é Marvin apenas para poder colocar meus objetos em um Dicionário.

Eu gosto da maneira como você coloca isso. Também gostei que você mencionou o próprio Dicionário. IDictionary é algo que pode ter toneladas de implementos diferentes com todos os tipos de qualidades diferentes (consulte as APIs de coleções em muitas plataformas). No entanto, ainda fornecemos apenas um 'Dicionário' básico que faz um trabalho decente no geral, embora possa não se destacar em todas as categorias.

Eu acho que isso é o que uma tonelada de pessoas estão procurando em uma biblioteca de hashing. Algo que realiza o trabalho, mesmo que não seja perfeito para todos os fins.

@morganbr Eu acho que as pessoas simplesmente querem uma maneira de escrever GetHashCode que seja melhor do que o que estão fazendo hoje (geralmente alguma combinação improvisada de operações matemáticas que eles copiaram de algo na web). Se você puder apenas fornecer um implemento básico daquelas runas bem, então as pessoas ficarão felizes. Você pode então ter uma API de bastidores para usuários avançados se eles tiverem uma forte necessidade de funções específicas de hash.

Em outras palavras, as pessoas que escrevem hashcodes hoje não vão saber ou se importar por que gostariam de Spooky vs Marvin vs Murmur. Apenas alguém com uma necessidade específica de um desses códigos hash específicos iria procurar. Mas muitas pessoas precisam dizer "aqui está o estado do meu objeto, forneça-me uma maneira de produzir um hash bem distribuído que seja rápido para que eu possa usar com dicionários, e que eu acho que me impede de ser DOSeado se acontecer para pegar uma entrada não confiável, hash e armazená-la ".

@CyrusNajmabadi O problema é que se estendermos nossas noções atuais de compatibilidade para o futuro, descobriremos que, uma vez que esse tipo seja lançado, ele nunca mais poderá mudar (a menos que descubramos que o algoritmo está terrivelmente quebrado de uma maneira "torna todos os aplicativos atacáveis" )

Uma vez, pode-se argumentar que, se começar como uma maneira aleatória e estável, será fácil alterar a implementação, já que você não poderia depender do valor de execução para execução de qualquer maneira. Mas se alguns anos depois descobrirmos que há um algoritmo que fornece balanceamento tão bom se não melhor de intervalos de hash com desempenho de caso melhor no geral, mas cria uma estrutura envolvendo uma Lista \

A sugestão de Morgan é que o código que você escreve hoje terá efetivamente as mesmas características de desempenho para sempre. Para os aplicativos que poderiam ter ficado melhores, isso é lamentável. Para as aplicações que teriam piorado, isso é fantástico. Mas quando encontramos o novo algoritmo, o verificamos e mudamos Roslyn (e sugerimos uma mudança para ReSharper / etc) para começar a gerar coisas com NewAwesomeThing2019 em vez de SomeThingThatWasConsideredAwesomeIn2018.

Qualquer coisa super caixa preta como essa só pode ser feita uma vez. E então ficamos presos a ele para sempre. Então alguém escreve o próximo, que tem melhor desempenho médio, então há duas implementações de caixa preta que você não sabe por que escolheria entre elas. E então ... e então ....

Então, claro, você pode não saber por que Roslyn / ReSharper / etc escreveu automaticamente GetHashCode para você usando Marvin32, ou Murmur, ou FastHash, ou uma combinação / condicional baseada em IntPtr.Size. Mas você tem o poder de investigar isso. E você tem o poder de alterá-lo em seus tipos mais tarde, conforme novas informações forem reveladas ... mas também demos a você o poder de mantê-lo igual. (Seria triste se escrevermos isso, e em 3 anos Roslyn / ReSharper / etc estão explicitamente evitando chamá-lo, porque o novo algoritmo é Muito Melhor ... Normalmente).

@bartonjs O que torna o hashing diferente de todos os lugares onde .Net fornece algoritmo de caixa preta ou estrutura de dados? Por exemplo, classificação (introsort), Dictionary (encadeamento separado baseado em array), StringBuilder (lista vinculada de 8k blocos), a maior parte do LINQ.

Analisamos isso mais profundamente hoje. Pedimos desculpas pelo atraso e pelas idas e vindas sobre este assunto.

Requisitos

  • Para quem é a API?

    • A API não precisa produzir um hash criptográfico forte

    • Mas: a API precisa ser boa o suficiente para que possamos usá-la no próprio framework (por exemplo, no BCL e ASP.NET)

    • No entanto, isso não significa que temos que usar a API em todos os lugares. Tudo bem se houver partes do FX em que desejamos usar um personalizado para riscos de segurança / DOS ou por causa do desempenho. Exceções sempre existirão .

  • Quais são as propriedades desejadas desse hash?

    • Todos os bits na entrada são usados

    • O resultado é bem distribuído

    • A API fornecerá "um" código hash, mas não garante um algoritmo de código hash específico. Isso nos permite usar um algoritmo diferente posteriormente ou usar algoritmos diferentes em arquiteturas diferentes.

    • A API garantirá que dentro de um determinado processo os mesmos valores produzirão o mesmo código hash. Diferentes instâncias do mesmo aplicativo provavelmente produzirão códigos hash diferentes devido à randomização. Isso nos permite garantir que os consumidores não possam persistir com os valores de hash e, acidentalmente, confiar que eles sejam estáveis ​​em todas as execuções (ou pior, nas versões da plataforma).

Forma API

`` `C #
// Vai morar no conjunto central
// .NET Framework: mscorlib
// .NET Core: System.Runtime / System.Private.CoreLib
sistema de namespace
{
public struct HashCode
{
public static int Combine(T1 valor1);
public static int Combine(T1 valor1, T2 valor2);
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);
}

O caso mais complicado é quando o desenvolvedor precisa ajustar como o hash está sendo calculado. A ideia é que o site de chamada passe o hash desejado em vez do objeto / valor, assim:

`` `C #
público parcial classe Cliente
{
substituição pública int GetHashCode () =>
HashCode.Combine (
Identificação,
StringComparer.OrdinalIgnoreCase.GetHashCode (FirstName),
StringComparer.OrdinalIgnoreCase.GetHashCode (LastName),
);
}

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 passos

Este problema permanecerá em aberto. Para implementar a API, precisamos decidir qual algoritmo usar.

@morganbr fará uma proposta para bons candidatos. De modo geral, não queremos escrever um algoritmo de hash do zero - queremos usar um bem conhecido cujas propriedades sejam bem compreendidas.

No entanto, devemos medir a implementação de cargas de trabalho .NET típicas e ver qual algoritmo produz bons resultados (taxa de transferência e distribuição). É provável que as respostas sejam diferentes de acordo com a arquitetura da CPU, portanto, devemos considerar isso ao medir.

@jamesqo , ainda tem interesse em trabalhar nessa área? Nesse caso, atualize a proposta em conformidade.

@terrajobst , também podemos querer public static int Combine<T1>(T1 value); . Eu sei que parece um pouco engraçado, mas forneceria uma maneira de difundir bits de algo com um espaço hash de entrada limitado. Por exemplo, muitos enums têm apenas alguns hashes possíveis, usando apenas os poucos bits inferiores do código. Algumas coleções são construídas com base no pressuposto de que os hashes são espalhados por um espaço maior, portanto, a difusão dos bits pode ajudar a coleção a funcionar com mais eficiência.

public void Add(string value, StrinComparison comparison);

Nota: O parâmetro StringComparison deve ser nomeado comparisonType para coincidir com a nomenclatura usada em todos os outros lugares StringComparison é usado como um parâmetro.

Os critérios que nos ajudariam a escolher algoritmos seriam:

  1. O algoritmo tem um bom efeito de avalanche? Ou seja, cada bit de entrada tem 50% de chance de inverter cada bit de saída? Este site tem um estudo de vários algoritmos populares.
  2. O algoritmo é rápido para pequenas entradas? Como o HashCode.Combine geralmente processa 8 ou menos ints, o tempo de inicialização pode ser mais importante do que a taxa de transferência. Este site tem um conjunto interessante de dados para começar. Também é aqui que podemos precisar de respostas diferentes para diferentes arquiteturas ou outros pivôs (SO, AoT vs JIT, etc).

O que realmente gostaríamos de ver são os números de desempenho para candidatos escritos em C #, para que possamos estar razoavelmente seguros de que suas características serão válidas para .NET. Se você escrever um candidato e não o escolhermos para isso, ainda será um trabalho útil sempre que eu realmente obtiver a proposta de API para a API hash não criptográfica.

Aqui estão alguns candidatos que acho que valem a pena avaliar (mas sinta-se à vontade para propor outros):

  • Marvin32 (já temos uma implementação C # aqui ). Sabemos que é rápido o suficiente para String.GetHashCode e acreditamos que é resistente a HashDoS
  • xxHash32 (algoritmo mais rápido em x86 aqui que tem qualidade superior de acordo com SMHasher)
  • FarmHash (Mais rápido em x64 aqui . Não encontrei um bom indicador de qualidade para ele. Este pode ser difícil de escrever em C # embora)
  • xxHash64 (truncado para 32 bits) (Este não é um vencedor claro de velocidade, mas pode ser fácil de fazer se já tivermos xxHash32)
  • SpookyHash (tende a se sair bem em conjuntos de dados maiores)

É uma pena que os métodos Add não possam ter um tipo de retorno ref HashCode e retornar ref this para que possam ser usados ​​de maneira fluente,

As devoluções de readonly ref permitiriam isso? / cc @jaredpar @VSadov

AVISO: Se alguém escolher uma implementação de hash de uma base de código existente em algum lugar da Internet, mantenha o link para a fonte e verifique a licença (teremos que fazer isso também).

Se a licença não for compatível, podemos precisar escrever o algoritmo do zero.

IMO, usar os métodos Add deve ser extremamente incomum. Será para cenários muito avançados, e a necessidade de ser 'fluente' realmente não existirá.

Para os casos de uso comuns de 99% de todos os casos de código de usuário, deve-se ser capaz de simplesmente usar => HashCode.Combine(...) e ficar bem.

@morganbr

também podemos querer public static int Combine<T1>(T1 value); . Eu sei que parece um pouco engraçado, mas forneceria uma maneira de difundir bits de algo com um espaço hash de entrada limitado

Faz sentido. Eu adicionei.

@justinvp

Nota: O parâmetro StringComparison deve ser nomeado comparisonType para coincidir com a nomenclatura usada em todos os outros lugares StringComparison é usado como um parâmetro.

Fixo.

@CyrusNajmabadi

IMO, usar os métodos Add deve ser extremamente incomum. Será para cenários muito avançados, e a necessidade de ser 'fluente' realmente não existirá.

Concordou.

@benaadams - re: ref retornando this de Add - não, this não pode ser retornado por ref em métodos de estrutura, pois pode ser um rValue ou um temporário.

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

// r se refere a alguma variável aqui. Qual deles? Qual é o escopo / tempo de vida?
r = SomethingElse ();
`` `

Caso seja útil para fins de comparação, alguns anos atrás, transferi a função hash lookup3 Jenkins ( fonte C ) para C # aqui .

Estou me perguntando sobre as coleções:

@terrajobst

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

Por que há uma sobrecarga para matrizes, mas não para coleções gerais (ou seja, IEnumerable<T> )?

Além disso, não vai ser confuso que HashCode.Combine(array) e hashCode.Add((object)array) se comportem de uma maneira (use igualdade de referência) e hashCode.Add(array) se comportem de outra maneira (combina códigos hash dos valores em a matriz)?

@CyrusNajmabadi

Para os casos de uso comuns de 99% de todos os casos de código de usuário, deve-se ser capaz de usar apenas => HashCode.Combine(...) e ficar bem.

Se o objetivo é realmente ser capaz de usar Combine em 99% dos casos de uso (e não, digamos, 80%), então Combine não deveria de alguma forma suportar coleções de hash com base nos valores na coleção? Talvez devesse haver um método separado para fazer isso (um método de extensão ou um método estático em HashCode )?

Se Adicionar é um cenário de poder, devemos assumir que o usuário deve escolher entre Object.GetHashCode e combinar elementos individuais de coleções? Se isso ajudar, podemos considerar a renomeação das versões do array (e possíveis versões 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);
Eu me pergunto se também precisaríamos de sobrecargas com IEqualityComparers.

Proposta: fazer com que a estrutura do construtor implemente IEnumerable para oferecer suporte à sintaxe do inicializador de coleção:

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

Isso é muito mais elegante do que chamar Add() manualmente (em particular, você não precisa de uma variável temporária) e ainda não tem alocações.

mais detalhes

@SLaks Talvez essa sintaxe mais agradável pudesse esperar por https://github.com/dotnet/csharplang/issues/455 (assumindo que a proposta tivesse suporte), de modo que HashCode não teria que implementar o falso IEnumerable ?

Decidimos não substituir GetHashCode () para produzir o código hash, pois isso seria estranho, tanto em termos de nomenclatura quanto do ponto de vista comportamental (GetHashCode () deve retornar o código hash do objeto, não aquele que está sendo calculado).

Acho estranho que GetHashCode não retorne o hashcode calculado. Acho que isso vai confundir os desenvolvedores. Por exemplo, @SLaks já o usou em sua proposta em vez de usar ToHashCode .

@justinvp Se GetHashCode() não vai retornar o código hash computado, ele provavelmente deve ser marcado [Obsolete] e [EditorBrowsable(Never)] .

Por outro lado, não vejo mal em retornar o código hash calculado.

@terrajobst

Decidimos não substituir GetHashCode() para produzir o código hash, pois isso seria estranho, tanto em termos de nomenclatura quanto do ponto de vista comportamental ( GetHashCode() deve retornar o código hash do objeto, não aquele sendo computado).

Sim, GetHashCode() deve retornar o código hash do objeto, mas há alguma razão para que os dois códigos hash sejam diferentes? Ainda está correto, já que duas instâncias de HashCode com o mesmo estado interno retornarão o mesmo valor de GetHashCode() .

@terrajobst Acabei de ver seu comentário. Perdoe-me pela demora na resposta, demorei a olhar para a notificação porque pensei que seria apenas mais idas e vindas que não levariam a lugar nenhum. Fico feliz em ver que não é o caso! : tada:

Eu ficaria muito satisfeito em pegar isso e fazer a medição da taxa de transferência / distribuição (presumo que seja isso que você quis dizer com "interessado em trabalhar nesta área"). No entanto, dê-me um segundo para terminar de ler todos os comentários aqui.

@terrajobst

Podemos mudar

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

? Mudei o nome de Add -> AddRange para evitar o comportamento mencionado por @svick . Eu removi as sobrecargas de byte pois podemos nos especializar usando typeof(T) == typeof(byte) dentro do método se precisarmos fazer algo específico de byte. Além disso, mudei value -> values e length -> count . Também faz sentido ter uma sobrecarga do comparador.

@terrajobst Você pode me lembrar por quê

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

é necessário quando temos

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

?

@svick

@justinvp Se GetHashCode () não vai retornar o código hash calculado, ele provavelmente deve ser marcado como [Obsoleto] e [EditorBrowsable (Nunca)].

: +1:

@terrajobst Podemos voltar a ter uma conversão implícita de HashCode -> int , então nenhum método ToHashCode ? editar: ToHashCode está bem. Veja a resposta de @CyrusNajmabadi abaixo.

@jamesqo StringComparison é um enum.
No entanto, as pessoas poderiam usar o equivalente StringComparer .

Podemos voltar a ter uma conversão implícita de HashCode -> int, sem o método ToHashCode?

Discutimos isso e decidimos contra isso na reunião. O problema é que, quando o usuário obtém o 'int' final, esse trabalho extra geralmente é feito. isto é, o código interno do hashcode frequentemente fará uma etapa de finalização e pode se redefinir para um novo estado. Ter isso acontecendo com uma conversão implícita seria estranho. Se você fez isso:

HashCode hc = ...

int i1 = hc;
int i2 = hc;

Então você pode obter resultados diferentes.

Por esse motivo, também não gostamos da conversão explícita (já que as pessoas não pensam em conversões como uma mudança de estado interno).

Com um método, podemos documentar explicitamente que isso está acontecendo. Podemos até mesmo nomeá-lo para transmitir o máximo. ou seja, "ToHashCodeAndReset" (embora tenhamos decidido contra isso). Mas pelo menos o método pode ter uma documentação clara que o usuário pode ver em coisas como o intellisense. Esse não é realmente o caso com conversões.

Eu removi as sobrecargas de bytes, pois podemos nos especializar usando typeof (T) == typeof (byte)

IIRC havia alguma preocupação sobre isso não ser ok do ponto de vista do JIT. Mas isso pode ter sido apenas para os casos "typeof ()" do tipo não-valor. Contanto que o jit efetivamente faça a coisa certa para os casos typeof () do tipo de valor, então isso deve ser bom.

@CyrusNajmabadi Eu não sabia que a conversão para int poderia envolver um estado de mutação. ToHashCode então.

Para quem está pensando na perspectiva da criptografia - http://tuprints.ulb.tu-darmstadt.de/2094/1/thesis.lehmann.pdf

@terrajobst , você teve tempo para ler meus comentários (começando por aqui ) e decidir se você aprova o formato da API ajustada? Em caso afirmativo, acho que isso pode ser marcado como aprovado pela API / disponível e podemos começar a decidir sobre um algoritmo de hash.

@blowdart , alguma parte específica que você gostaria de destacar?

Posso não ter sido muito explícito sobre isso acima, mas os únicos hashes não criptográficos que não conheço de quebras de HashDoS são Marvin e SipHash. Ou seja, mesmo semeando (digamos) Murmur com um valor aleatório ainda pode ser quebrado e usado para um DoS.

Nenhum, apenas achei interessante e estou pensando que a documentação para isso deveria dizer "Não deve ser usado em códigos hash que são gerados por meio de algoritmos criptográficos."

Decisões

  • Devemos remover todos os métodos AddRange porque o cenário não está claro. É pouco provável que os arrays apareçam com muita frequência. E uma vez que matrizes maiores estão envolvidas, a questão é se a computação deve ser armazenada em cache. Ver o loop for do lado da chamada deixa claro que você precisa pensar sobre isso.
  • Também não queremos adicionar sobrecargas de IEnumerable a AddRange porque elas seriam alocadas.
  • Não achamos que precisamos da sobrecarga para Add que leva string e StringComparison . Sim, eles são provavelmente mais eficientes do que chamar por meio de IEqualityComparer , mas podemos consertar isso mais tarde.
  • Achamos que marcar GetHashCode como obsoleto com erro é uma boa ideia, mas iríamos um passo adiante e também nos esconderíamos do IntelliSense.

Isso nos deixa com:

`` `C #
// Vai morar no conjunto central
// .NET Framework: mscorlib
// .NET Core: System.Runtime / System.Private.CoreLib
sistema de namespace
{
public struct HashCode
{
public static int Combine(T1 valor1);
public static int Combine(T1 valor1, T2 valor2);
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óximas etapas: O problema está em aberto - para implementar a API, precisamos com vários algoritmos candidatos como experimentos - consulte https://github.com/dotnet/corefx/issues/14354#issuecomment -305028686 para obter a lista, para que possamos decidir qual algoritmo usar (com base nas medidas de taxa de transferência e distribuição, provavelmente uma resposta diferente por arquitetura de CPU).

Complexidade: Grande

Se alguém estiver interessado em pegá-lo, por favor, envie um email para nós. Pode até haver espaço para várias pessoas trabalhando juntas. ( @jamesqo você tem escolha de prioridade, já que investiu mais e mais tempo no problema)

@karelz Apesar do meu comentário acima , mudei de ideia porque não acho que tenho as qualificações para escolher o melhor algoritmo de hash. Eu olhei em algumas das bibliotecas @morganbr listadas e percebi que a implementação é bastante complexa , então não posso traduzi-la facilmente para C # para testar por mim mesmo. Tenho pouca experiência em C ++, então também teria dificuldade em apenas instalar a biblioteca e escrever um aplicativo de teste.

Não quero que isso permaneça na lista de opções para sempre, no entanto. Se ninguém demorar uma semana a partir de hoje, considerarei a possibilidade de postar uma pergunta no Programmers SE ou Reddit.

Eu não testei (ou otimizei de outra forma), mas aqui está uma implementação básica do algoritmo de hash Murmur3 que uso em vários de meus projetos pessoais: https://gist.github.com/tannergooding/0a12559d1a912068b9aeb4b9586aad7f

Acho que a solução mais ideal aqui será alterar dinamicamente o algoritmo de hash com base no tamanho dos dados de entrada.

Ex: Mumur3 (e outros) são muito rápidos para grandes conjuntos de dados e fornecem ótima distribuição, mas podem ter um desempenho 'ruim' (em termos de velocidade, não de distribuição) para conjuntos de dados menores.

Imagino que devamos fazer algo como: Se a contagem geral de bytes for menor que X, faça o algoritmo A; caso contrário, faça o algoritmo B. Isso ainda será determinístico (por execução), mas nos permitirá fornecer velocidade e distribuição com base no tamanho real dos dados de entrada.

Provavelmente também vale a pena notar que vários dos algoritmos mencionados têm implementações projetadas especificamente para instruções SIMD, então uma solução de melhor desempenho provavelmente envolveria um FCALL em algum nível (como é feito com algumas das implementações BufferCopy) ou pode envolver tomar uma dependência em System.Numerics.Vector .

@jamesqo , estamos felizes em ajudar a fazer escolhas; o que mais precisamos de ajuda são os dados de desempenho para implementações candidatas (idealmente C #, embora, como @tannergooding aponta, alguns algoritmos precisem de suporte de compilador especial). Como mencionei acima, se você construir um candidato que não foi escolhido, provavelmente o usaremos mais tarde, portanto, não se preocupe com a perda de trabalho.

Eu sei que existem benchmarks por aí para várias implementações, mas acho que é importante ter uma comparação usando esta API e um intervalo provável de entradas (por exemplo, structs com 1-10 campos).

@tannergooding , esse tipo de adaptabilidade pode ter melhor desempenho, mas não vejo como funcionaria com o método Add, pois não sabe quantas vezes será chamado. Embora pudéssemos fazer isso com Combine, isso significaria que uma série de chamadas Add poderia produzir um resultado diferente do que a chamada Combine correspondente.

Além disso, dado que o intervalo de entradas mais provável é de 4 a 32 bytes ( Combine`1 - Combine`8 ), espero que não haja grandes mudanças de desempenho nesse intervalo.

esse tipo de adaptabilidade pode ter melhor desempenho, mas não vejo como funcionaria com o método Add, pois não sabe quantas vezes será chamado.

Não estou pessoalmente convencido de que o formato da API seja adequado para hash de uso geral (está próximo, no entanto) ...

Atualmente, estamos expondo Combine métodos para construção estática. Se eles pretendem combinar todas as entradas e produzir um código hash finalizado, o nome é 'ruim' e algo como Compute pode ser mais apropriado.

Se estivermos expondo Combine métodos, eles devem apenas misturar todas as entradas e os usuários devem ser solicitados a chamar um método Finalize que obtém a saída da última combinação, bem como o número total de bytes que foram combinados para produzir um código hash finalizado (finalizar um código hash é importante, pois é o que causa a avalanche de bits).

Para o padrão do construtor, estamos expondo um método Add e ToHashCode . Não está claro se o método Add se destina a armazenar os bytes e apenas combinar / finalizar na chamada para ToHashCode (nesse caso, podemos escolher o algoritmo correto dinamicamente) ou se eles são destinado a ser combinado em tempo real, deve ficar claro que esse é o caso (e que a implementação deve estar rastreando internamente o tamanho total dos bytes combinados).

Para quem procura um ponto de partida menos complicado, experimente xxHash32. É provável que se traduza facilmente para C # (as pessoas já fizeram isso ).

Ainda testando localmente, mas estou vendo as seguintes taxas de transferência para minha implementação C # de Murmur3.

Estes são para os métodos estáticos Combine 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

Minha implementação assume que GetHashCode deve ser chamado para cada entrada e que o valor calculado deve ser finalizado antes de ser retornado.

Combinei int valores, pois são os mais simples de testar.

Para calcular a taxa de transferência, executei 10.001 iterações, descartando a primeira iteração como a execução de 'aquecimento'.

Em cada iteração, executo 10.000 sub-iterações onde chamo HashCode.Combine , passando o resultado da sub-iteração anterior como o primeiro valor de entrada na próxima iteração.

Em seguida, faço a média de todas as iterações para obter o tempo médio decorrido, divido ainda mais pelo número de sub-iterações executadas por loop para obter o tempo médio por chamada. Em seguida, calculo o número de chamadas que podem ser feitas por segundo e multiplico pelo número de bytes combinados para calcular a taxa de transferência real.

Irá limpar o código e compartilhá-lo um pouco.

@tannergooding , isso parece um grande progresso. Para ter certeza de que você está obtendo as medidas corretas, a intenção da API é que uma chamada para HashCode.Combine(a, b) seja equivalente a chamar

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

Em ambos os casos, os dados devem ser alimentados no mesmo estado de hash interno e o hash deve ser finalizado uma vez no final.

👍

Isso é efetivamente o que o código que escrevi está fazendo. A única diferença é que eu efetivamente inlinei todo o código (não há necessidade de alocar new HashCode() e rastrear o número de bytes combinados, uma vez que é constante).

@morganbr. Implementação + teste de rendimento para Murmur3: https://gist.github.com/tannergooding/89bd72f05ab772bfe5ad3a03d6493650

MurmurHash3 é baseado no algoritmo descrito aqui: https://github.com/aappleby/smhasher/wiki/MurmurHash3 , o repo diz que é MIT

Trabalhando em xxHash32 (cláusula BSD-2 - https://github.com/Cyan4973/xxHash/blob/dev/xxhash.c) e SpookyHash (Domínio público - http://www.burtleburtle.net/bob/hash /spooky.html) variantes

@tannergooding Mais uma vez, não sou especialista em hash, mas me lembrei [lendo um artigo] [1] que dizia que Murmur não era resistente a DoS, então, apenas apontar isso antes de escolhermos isso.

@jamesqo , posso estar errado, mas tenho quase certeza de que a vulnerabilidade se aplica ao Murmur2 e não ao Murmur3.

Em ambos os casos, estou implementando vários algoritmos para que possamos obter resultados de taxa de transferência para C #. A distribuição e outras propriedades desses algoritmos são bastante conhecidas, então podemos escolher qual é a melhor depois 😄

Opa, esqueci de vincular o artigo: http://emboss.github.io/blog/2012/12/14/breaking-murmur-hash-flooding-dos-reloaded/.

@tannergooding OK. Parece justo: +1:

@tannergooding , dei uma olhada em sua implementação Murmur3 e geralmente parece correta e provavelmente muito bem otimizada. Para ter certeza de que entendi corretamente, você está usando o fato de que mixedValue e o estado interno de Murmur são ambos de 32 bits? Essa é provavelmente uma otimização muito boa para este caso e explica algumas das minhas confusões anteriores.

Se formos adotá-lo, pode ser necessário alguns ajustes (eles provavelmente não farão uma grande diferença nas medições de desempenho):

  • Combinarainda deve chamar CombineValue em valor1
  • As primeiras chamadas CombineValue devem ter uma semente aleatória
  • ToHashCode deve redefinir _bytesCombined e _combinedValue

Enquanto isso, enquanto anseio por esta API, quão ruim é para mim implementar GetHashCode via (field1, field2, field3).GetHashCode() ?

@ jnm2 , o combinador de código hash ValueTuple tende a colocar suas entradas em ordem no código hash (e descartar os menos recentes). Para alguns campos e uma tabela hash que divide por um número primo, você pode não notar. Para muitos campos ou uma tabela hash que divide por uma potência de dois, a entropia do último campo que você inserir terá a maior influência sobre se você tem colisões (por exemplo, se seu último campo é um bool ou um pequeno int, você provavelmente terá muitas colisões; se for um guid, provavelmente não terá).

ValueTuple também não funciona bem com campos que são todos 0.

Por outro lado, tive que parar de trabalhar em outras implementações (ter trabalho de prioridade mais alta). Não tenho certeza de quando poderei pegá-lo de volta.

Portanto, se isso não é bom o suficiente para um tipo estruturado, por que é bom o suficiente para uma tupla?

@ jnm2 , esse é um dos motivos pelos quais vale a pena construir esse recurso - para que possamos substituir hashes abaixo do padrão em toda a estrutura.

Grande tabela de funções hash com características de desempenho e qualidade:
https://github.com/leo-yuriev/t1ha

@arespr Acho que a equipe está procurando uma implementação C # das funções hash. Obrigado por compartilhar, no entanto.

@tannergooding Você ainda não conseguiu pegar este problema de volta? Nesse caso, postarei no Reddit / Twitter que estamos procurando um especialista em hash.

editar: Postado no Reddit. https://www.reddit.com/r/csharp/comments/6qsysm/looking_for_hash_expert_to_help_net_core_team/?ref=share&ref_source=link

@jamesqo , tenho algumas coisas de maior prioridade em meu prato e não poderei fazer isso nas próximas 3 semanas.

Além disso, as medições atuais serão limitadas pelo que podemos codificar atualmente em C #, no entanto, se / quando isso se tornar uma coisa (https://github.com/dotnet/designs/issues/13), as medições provavelmente mudarão um pouco ;)

Além disso, as medições atuais serão limitadas pelo que podemos codificar atualmente em C #, no entanto, se / quando isso se tornar uma coisa (dotnet / designs # 13), as medições provavelmente mudarão um pouco;)

Tudo bem - podemos sempre alterar o algoritmo de hash uma vez que os intrínsecos se tornem disponíveis, encapsulando / randomizando o código de hash nos permite fazer isso. Estamos apenas procurando por algo que ofereça a melhor relação desempenho / distribuição para o tempo de execução em seu estado atual.

@jamesqo , obrigado por procurar gente para ajudar. Ficaríamos felizes em ter alguém que não seja um especialista em hash trabalhando nisso também - realmente só precisamos de alguém que possa portar alguns algoritmos para C # de outras linguagens ou designs e, em seguida, fazer medições de desempenho. Assim que escolhermos os candidatos, nossos especialistas farão o que fizermos em relação a qualquer alteração - revisar o código quanto à exatidão, desempenho, segurança, etc.

Oi! Acabei de ler a discussão e, pelo menos para mim, parece que o caso está fortemente encerrado a favor do murmúrio3-32 PoC. Que BTW parece ser uma escolha muito boa para mim, e eu recomendo não gastar mais nenhum trabalho desnecessário (mas talvez até mesmo descartar os .Add() membros ...).

Mas no caso improvável de alguém querer continuar com mais trabalho de desempenho, eu poderia fornecer algum código para xx32, xx64, hsip13 / 24, seahash, murmur3-x86 / 32 (e eu integrei o impl marvin32 de cima), e (ainda unoptimized) sip13 / 24, spookyv2. Algumas versões do City parecem fáceis de transportar, caso seja necessário. Esse projeto meio abandonado tinha um caso de uso ligeiramente diferente em mente, portanto, não há classe HashCode com a API proposta; mas para benchmarking não deve importar muito.

Definitivamente não está pronto para a produção: o código aplica quantidades generosas de força bruta como massa de cópia, expansão cancerosa de agressivo em linha e inseguro; endianess não existe, nem leituras desalinhadas. Mesmo os testes contra vetores de teste ref-impl estão eufemisticamente falando "incompletos".

Se isso for de alguma ajuda, devo encontrar tempo suficiente durante as próximas duas semanas para consertar os problemas mais flagrantes e disponibilizar o código e alguns resultados preliminares.

@gimpf

Acabei de ler a discussão e, pelo menos para mim, parece que o caso está fortemente encerrado a favor do murmúrio3-32 PoC. Que BTW parece uma escolha muito boa para mim, e eu recomendo não gastar mais nenhum trabalho desnecessário

Não, as pessoas não estão favorecendo Murmur3 ainda. Queremos ter certeza de que estamos escolhendo o melhor algoritmo absoluto em termos de equilíbrio entre desempenho / distribuição, portanto, não podemos deixar pedra sobre pedra.

Mas no caso improvável de alguém querer continuar com mais trabalho de desempenho, eu poderia fornecer algum código para xx32, xx64, hsip13 / 24, seahash, murmur3-x86 / 32 (e eu integrei o impl marvin32 de cima), e (ainda unoptimized) sip13 / 24, spookyv2. Algumas versões do City parecem fáceis de transportar, caso seja necessário.

Sim por favor! Queremos coletar código para o maior número possível de algoritmos para testar. Cada novo algoritmo com o qual você pode contribuir é valioso. Seria muito grato se você pudesse portar os algoritmos da cidade também.

Definitivamente não está pronto para a produção: o código aplica quantidades generosas de força bruta como massa de cópia, expansão cancerosa de agressivo em linha e inseguro; endianess não existe, nem leituras desalinhadas. Mesmo os testes contra vetores de teste ref-impl estão eufemisticamente falando "incompletos".

Isso está ok. Basta trazer o código e outra pessoa poderá encontrá-lo, se necessário.

Se isso for de alguma ajuda, devo encontrar tempo suficiente durante as próximas duas semanas para consertar os problemas mais flagrantes e disponibilizar o código e alguns resultados preliminares.

Sim, isso seria ótimo!

@jamesqo Ok, vou deixar uma nota assim que tiver algo para mostrar.

@gimpf, isso soa muito bem e adoraríamos ouvir sobre seu progresso conforme você avança (não há necessidade de esperar até que você comece a trabalhar em cada algoritmo!). Não está pronto para produção, desde que você acredite que o código produz resultados corretos e que o desempenho é uma boa representação do que veríamos em uma implementação pronta para produção. Assim que escolhermos os candidatos, podemos trabalhar com você para obter implementações de alta qualidade.

Eu não vi uma análise de como a entropia de seahash se compara a outros algoritmos. Você tem alguma indicação sobre isso? Ele tem compensações interessantes de desempenho ... a vetorização parece rápida, mas a aritmética modular parece lenta.

@morganbr , tenho um teaser pronto.

Sobre SeaHash : Não, eu não sei sobre a qualidade ainda; caso o desempenho seja interessante, eu adicionaria ao SMHasher. Pelo menos o autor afirma que é bom (usá-lo para somas de verificação em um sistema de arquivos) e também afirma que nenhuma entropia é desperdiçada durante a mixagem.

Sobre os hashes e benchmarks : Projeto Haschisch.Kastriert , página wiki com os primeiros resultados de benchmarking comparando xx32, xx64, hsip13, hsip24, marvin32, sea e murmur3-32.

Algumas advertências importantes:

  • Esta foi uma corrida de bancada muito rápida com configurações de baixa precisão.
  • As implementações ainda não foram concluídas e alguns concorrentes ainda estão faltando. As implementações de Streaming (tal coisa seria necessária para um suporte sensato .Add ()) precisam de uma otimização real.
  • SeaHash atualmente não está usando uma semente.

Primeiras impressões:

  • para mensagens grandes, xx64 é a mais rápida das implementações listadas (cerca de 3,25 bytes por ciclo, tanto quanto eu entendo, ou 9,5 GiB / s no meu notebook)
  • para mensagens curtas, nada é ótimo, mas murmur3-32 e (surpreendentemente) seahash têm uma vantagem, mas a última provavelmente é explicada por seahash ainda não usando uma semente.
  • o "benchmark" para acessar HashSet<> precisa ser trabalhado, pois tudo está quase dentro do erro de medição (eu vi diferenças maiores, mas ainda não vale a pena falar sobre)
  • ao combinar códigos hash, o murmur-3A PoC é cerca de 5 a 20 vezes mais rápido do que o que temos aqui
  • algumas abstrações em C # são muito caras; isso torna a comparação de algoritmos de hash mais incômoda do que o necessário.

Escreverei novamente assim que melhorar um pouco a situação.

@gimpf , isso é um começo fantástico! Dei uma olhada no código e nos resultados e tenho algumas perguntas.

  1. Seus resultados mostram SimpleMultiplyAdd como cerca de 5x mais lento do que Murmur3a de @tannergooding. Isso parece estranho, já que Murmur tem mais trabalho a fazer do que multiplicar + adicionar (embora eu reconheça que girar é uma operação mais rápida do que adicionar). É possível que suas implementações tenham uma ineficiência comum que não esteja nessa implementação do Murmur ou devo ler isso como implementações personalizadas que têm uma grande vantagem sobre as de uso geral?
  2. Ter resultados para 1, 2 e 4 combinações é bom, mas essa API vai até 8. Seria possível obter resultados para isso também ou isso causa muita duplicação?
  3. Eu vi que você executou em X64, então esses resultados devem nos ajudar na escolha de nosso algoritmo X64, mas outros benchmarks sugerem que algoritmos podem diferir dramaticamente entre X86 e X64. É fácil para você também obter resultados do X86? (Em algum ponto, também precisaríamos obter o ARM e o ARM64, mas esses definitivamente podem esperar)

Seus resultados de HashSet são particularmente interessantes. Se eles se mantiverem, esse é um caso possível para preferir uma entropia melhor em vez de um tempo de hash mais rápido.

@morganbr Este fim de semana foi mais

Sobre suas perguntas:

  1. Seus resultados mostram SimpleMultiplyAdd como cerca de 5x mais lento do que Murmur3a de @tannergooding. Isso parece estranho ...

Eu estava me perguntando. Esse foi um erro de copiar / colar, SimpleMultiplyAdd sempre combinava quatro valores ... Além disso, ao reordenar algumas instruções, o combinador multiply-add ficou um pouco mais rápido (rendimento ~ 60% maior).

É possível que suas implementações tenham uma ineficiência comum que não esteja nessa implementação do Murmur ou devo ler isso como implementações personalizadas que têm uma grande vantagem sobre as de uso geral?

Provavelmente perdi algumas coisas, mas parece que as implementações de propósito geral do .NET não podem ser usadas neste caso de uso. Eu escrevi métodos de estilo Combine para todos os algoritmos, e a combinação de código hash wrt tem desempenho _muito_ melhor do que os de propósito geral.

No entanto, mesmo essas implementações permanecem muito lentas; mais trabalho é necessário. O desempenho do .NET nessa área é absolutamente opaco para mim; adicionar ou remover uma cópia de uma variável local pode facilmente alterar o desempenho por um fator de dois. Provavelmente não serei capaz de fornecer implementações suficientemente bem otimizadas para o propósito de selecionar a melhor opção.

  1. Ter resultados para 1, 2 e 4 combinações é bom, mas esta API vai até 8.

Eu estendi os benchmarks de combinar. Sem surpresas nessa frente.

  1. Eu vi que você executou em X64 (...), é fácil para você também obter resultados de X86?

Já foi, mas depois mudei para o .NET Standard. Agora estou em um inferno de dependências, e apenas os benchmarks .NET Core 2 e CLR de 64 bits funcionam. Isso pode ser resolvido com bastante facilidade, uma vez que resolvi os problemas atuais.

Você acha que isso vai chegar na versão v2.1?

@gimpf Você não posta há algum tempo - você tem uma atualização do progresso de suas implementações? :risonho:

@jamesqo Corrigi alguns benchmarks que causavam resultados estranhos e adicionei City32, SpookyV2, Sip13 e Sip24 à lista de algoritmos disponíveis. Os Sips são tão rápidos quanto o esperado (em relação à taxa de transferência de xx64), City e Spooky não (o mesmo ainda é válido para SeaHash).

Para combinar códigos hash, Murmur3-32 ainda parece uma boa aposta, mas ainda não fiz uma comparação mais exaustiva.

Por outro lado, a API de streaming (.Add ()) tem o infeliz efeito colateral de remover alguns algoritmos de hash da lista de candidatos. Dado que o desempenho de tal API também é questionável, você pode querer repensar se deve oferecê-la desde o início.

Se a parte .Add() fosse evitada, e dado que o combinador de hash está usando uma semente, não acho que haveria qualquer mal em limpar o combinador de tg, criar um pequeno conjunto de testes e chamá-lo um dia. Como tenho apenas algumas horas todo fim de semana, e a otimização de desempenho é um tanto entediante, fazer a versão banhada a ouro pode se arrastar um pouco ...

@gimpf , isso parece um grande progresso. Você tem uma tabela de resultados à mão para que possamos ver se há o suficiente para tomar uma decisão e seguir em frente?

@morganbr Atualizei meus resultados de benchmarking .

Por enquanto, tenho apenas resultados de 64 bits no .NET Core 2. Para essa plataforma, City64 sem semente é o mais rápido em todos os tamanhos. Incorporando uma semente, XX-32 está vinculado a Murmur-3-32. Felizmente, esses são os mesmos algoritmos que têm a reputação de serem rápidos para plataformas de 32 bits, mas obviamente precisamos verificar se isso também é válido para a minha implementação. Os resultados parecem ser representativos do desempenho do mundo real, exceto que Sea e SpookyV2 parecem excepcionalmente lentos.

Você precisará considerar o quanto você realmente precisa de proteção de hash para combinadores de código de hash. Se a semeadura for necessária apenas para tornar o hash obviamente inutilizável para persistência, o city64 uma vez usado o XOR com uma semente de 32 bits seria uma melhoria. Como esse utilitário existe apenas para combinar hashes (e não substituir, por exemplo, o código hash para strings, ou ser um hasher drop-in para arrays inteiros, etc.), isso pode ser bom o suficiente.

Se OTOH você acha que precisa, você ficará feliz em ver que Sip13 é geralmente menos de 50% mais lento do que XX-32 (em plataformas de 64 bits), mas esse resultado provavelmente será significativamente diferente para aplicativos de 32 bits.

Não sei o quanto é relevante para o corefx, mas adicionei os resultados do LegacyJit de 32 bits (com FW 4.7).

Eu gostaria de dizer que os resultados são ridiculamente lentos. No entanto, como exemplo, a 56 MiB / s contra 319 MiB / s, não estou rindo (isso é Sip, está faltando a otimização de girar para a esquerda). Acho que me lembro porque cancelei meu projeto de algoritmo de hash .NET em janeiro ...

Então, RyuJit-32bit ainda está faltando e (com sorte) dará resultados muito diferentes, mas para LegacyJit-x86, Murmur-3-32 vence com facilidade, e apenas City-32 e xx-32 podem chegar perto. Murmur ainda tem um desempenho ruim em apenas cerca de 0,4 a 1,1 GB / s em vez de 0,6 a 2 GB / s (na mesma máquina), mas pelo menos está na estimativa certa.

Vou executar os benchmarks em algumas das minhas caixas esta noite e postar os resultados (Ryzen, i7, Xeon, A10, i7 Mobile e acho que alguns outros).

@tannergooding @morganbr Algumas atualizações interessantes e importantes.

Importante primeiro:

  • Corrigi algumas implementações de combinação que estavam produzindo valores de hash incorretos.
  • O conjunto de benchmark agora trabalha mais para evitar dobramentos constantes. City64 era suscetível (como era o sopro-3-32 no passado). Não significa que eu entenda todos os resultados agora, mas eles são muito mais plausíveis.

Coisas legais:

  • Implementações de combinador agora estão disponíveis para todos os 1 a 8 sobrecargas de argumento, incluindo as implementações um pouco mais complicadas desenroladas manualmente para xx / city.
  • Os testes e benchmarks também os verificam. Como muitos algoritmos de hash têm mensagens de byte baixo com maiúsculas e minúsculas especiais, essas medidas podem ser de interesse.
  • Benchmarks de execução simplificados para vários alvos (Core vs. FW).

Para executar um pacote em todas as implementações principais para combinar códigos hash, incluindo "Vazio" (sobrecarga pura) e "multiplicação-adição" (versão com velocidade otimizada da famosa resposta SO):

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

(_A execução de benchmarks de 32 bits Core convenientemente parece exigir o pré-lançamento do BenchmarkDotNet (ou talvez uma configuração de apenas 32 bits usando o bench-runner baseado em Core). Deve então funcionar usando -j: core_x86, espero) _

Resultados : Depois de toda correção de bugs, xx32 parece vencer para todas as sobrecargas com RyuJIT de 64 bits, no Windows 10 em um Haswell i7 móvel, em uma execução "rápida". Entre o Sips e o marvin32, o Sip-1-3 sempre vence. Sip-1-3 é cerca de 4 vezes mais lento do que xx32, que novamente é cerca de 2 vezes mais lento do que um combinador multiply-add primitivo. Os resultados do 32bit Core ainda estão faltando, mas estou mais ou menos esperando por uma versão estável do BenchmarkDotNet que resolverá esse problema para mim.

(Editar) Acabei de adicionar uma execução rápida de um benchmark para acessar um conjunto de hash . Obviamente, isso depende

Obrigado mais uma vez @gimpf pelos dados fantásticos! Vamos ver se podemos transformar isso em uma decisão.

Para começar, eu dividiria os algoritmos assim:
Entropia rápida + boa (ordenada por velocidade):

  1. xxHash32
  2. City64 (provavelmente será lento em x86, então provavelmente teremos que escolher outra coisa para x86)
  3. Murmur3A

Resistente a HashDoS:

  • Marvin32
  • SipHash. Se nos inclinarmos para isso, precisaremos fazer com que os especialistas em criptografia da Microsoft revisem os dados para confirmar se os resultados da pesquisa são aceitáveis. Também teremos que descobrir quais parâmetros são seguros o suficiente. O papel sugere algo entre Sip-2-4 e Sip-4-8.

Fora de contenção (lento):

  • SpookyV2
  • City32
  • xxHash64
    * SeaHash (e não temos dados sobre entropia)

Fora de contenção (má entropia):

  • MultiplyAdd
  • HSip

Antes de escolhermos um vencedor, gostaria de ter certeza de que outras pessoas concordam com meu balde acima. Se for o caso, acho que só precisamos escolher se pagaremos 2x pela resistência a HashDoS e seguir em frente.

@morganbr Seu agrupamento parece bom. Como um ponto de dados nas rodadas SipHash, o projeto Rust perguntou a Jean-Philippe Aumasson , que foi o autor do sip-hash com DJB. Depois dessa discussão, eles decidiram usar sip-1-3 para tabelas de hash.

(Veja ferrugem PR: # 33940 e o problema de ferrugem que acompanha

Com base nos dados e comentários, gostaria de propor que usemos xxHash32 em todas as arquiteturas. A próxima etapa é implementá-lo. @gimpf , você está interessado em fazer um RP para isso?

Para aqueles preocupados com HashDoS, irei seguir em breve com uma proposta para uma API de hashing de uso geral que deve incluir Marvin32 e pode incluir SipHash. Esse também será um local apropriado para as outras implementações nas quais @gimpf e @tannergooding trabalharam.

@morganbr Posso montar um PR se o tempo permitir. Além disso, eu pessoalmente prefiro xx32 também, desde que não reduza a aceitação.

@gimpf , como está seu tempo? Se você realmente não tiver tempo, também podemos ver se mais alguém gostaria de tentar.

@morganbr Eu planejava fazer isso até 5 de novembro, e ainda parece bom que encontrarei tempo nas próximas duas semanas.

@gimpf , parece ótimo. Obrigado pela atualização!

@terrajobst - Estou um pouco atrasado para a festa (desculpe), mas não podemos alterar o tipo de retorno do método Add?

`` `c #
public HashCode Adicionar(Valor T);
public HashCode Adicionar(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();

No entanto, exatamente a mesma coisa pode ser alcançada assim, embora com uma alocação de array menos dispendiosa:

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

Observe que os tipos também podem ser misturados. Obviamente, isso poderia ser feito ao não chamá-lo fluentemente dentro de um método regular. Dado este argumento de que a interface fluente não é absolutamente necessária, por que a sobrecarga de params desperdiçadora existe para começar? Se esta sugestão for uma sugestão ruim, então a sobrecarga de params cai no mesmo machado. Isso, e forçar um método regular para um hashcode trivial, mas ótimo, parece muita cerimônia.

Edit: Um implicit operator int também seria bom para o DRY, mas não exatamente crucial.

@jcdickinson

não podemos alterar o tipo de retorno do método Add?

Já discutimos isso na proposta antiga, e ela foi rejeitada.

por que existe a sobrecarga de parâmetros perdulários para começar?

Não estamos adicionando sobrecargas de parâmetros? Faça um Ctrl + F para "params" nesta página da web, e você verá que seu comentário é o único lugar onde essa palavra aparece.

Um operador implícito int também seria bom para DRY, mas não exatamente crucial.

Eu acredito que também foi discutido em algum lugar acima ...

@jamesqo obrigado pela explicação.

sobrecargas params

Eu quis dizer AddRange , mas acho que não haverá qualquer tração nisso.

@jcdickinson AddRange estava na proposta original, mas não está na versão atual. Ele foi rejeitado pela revisão da API (consulte https://github.com/dotnet/corefx/issues/14354#issuecomment-308190321 por @terrajobst):

Devemos remover todos os métodos AddRange porque o cenário não está claro. É improvável que as matrizes apareçam com muita frequência. E uma vez que matrizes maiores estão envolvidas, a questão é se a computação deve ser armazenada em cache. Ver o loop for do lado da chamada deixa claro que você precisa pensar sobre isso.

@gimpf Eu fui em frente e preenchi a proposta com xxHash32 . Sinta-se à vontade para pegar essa implementação. Ele tem testes contra vetores xxHash32 reais.

Editar

Em relação à interface. Tenho plena consciência de que estou transformando um pequeno morro em uma montanha - fique à vontade para ignorar. Estou usando a proposta atual contra coisas reais e é um monte de repetições irritantes.

Tenho brincado com a interface e agora entendo por que a interface fluente foi rejeitada; é significativamente mais 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

Usando um método não embutido como uma fonte de código hash; 50 invocações de Add vs um método de extensão fluente:

| Método | Média | Erro | StdDev | Dimensionado |
| ------- | ---------: | ---------: | ---------: | -------: |
| Adicionar | 401,6 ns | 1,262 ns | 1,180 ns | 1,00 |
| Tally | 747,8 ns | 2,329 ns | 2,178 ns | 1,86 |

No entanto, o seguinte padrão funciona:

`` `c #
public struct HashCode: System.Collections.IEnumerable
{
[EditorBrowsable (EditorBrowsableState.Never)]
[Obsoleto ("Este método é fornecido para a sintaxe do inicializador de coleção.", Erro: verdadeiro)]
public IEnumerator GetEnumerator () => lançar novo NotImplementedException ();
}

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

`` `

Também possui características de desempenho idênticas às da proposta atual:

| Método | Média | Erro | StdDev | Dimensionado |
| ------------ | ---------: | ---------: | ---------: | --- ----: |
| Adicionar | 405,0 ns | 2,130 ns | 1,889 ns | 1,00 |
| Initializer | 400,8 ns | 4,821 ns | 4,274 ns | 0,99 |

Infelizmente, é um hack, já que IEnumerable tem que ser implementado para manter o compilador feliz. Dito isso, o Obsolete irá gerar um erro até mesmo foreach - você teria que realmente querer quebrar as coisas para encontrar a exceção. O MSIL entre os dois é essencialmente idêntico.

@jcdickinson, obrigado por abordar o problema. Enviei-lhe um convite de Colaborador, diga-me quando aceitar e poderei atribuir-lhe este problema (entretanto atribuindo a mim mesmo).

Dica de profissional: depois de aceitar, o GitHub o inscreverá automaticamente para todas as notificações do repo (mais de 500 por dia). Recomendo alterá-lo para apenas "Não Assistindo", que enviará a você todas as suas menções e notificações de problemas você se inscreveu.

@jcdickinson , estou definitivamente interessado em maneiras de evitar repetições irritantes (embora não tenha ideia de como as pessoas se sentiriam sobre a sintaxe do inicializador). Parece que me lembro que havia dois problemas com fluente:

  1. O problema de desempenho que você notou
  2. O valor de retorno dos métodos fluent é uma cópia da estrutura. É muito fácil acidentalmente acabar perdendo informações fazendo coisas como:
var hc = new HashCode();
var newHc = hc.Add(foo);
hc.Add(bar);
return newHc.ToHashCode();

Como a proposta neste tópico já foi aprovada (e você está no caminho certo para mesclá-la), sugiro iniciar uma nova proposta de API para quaisquer alterações.

@karelz Eu acredito que @gimpf já pegou esse problema de antemão. Como ele tem mais familiaridade com a implementação, atribua esse problema a @gimpf . ( editar: nvm)

@terrajobst Um tipo de solicitação de API de última hora para isso. Como marcamos GetHashCode obsoleto, estamos dizendo implicitamente ao usuário que HashCode s não são valores que devem ser comparados, apesar de serem estruturas que são tipicamente imutáveis ​​/ comparáveis. Nesse caso, devemos marcar Equals obsoleto também?

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

Acho que algo semelhante foi feito com Span .

Se isso for aceito, então eu acho ...

  1. Eu consideraria usar should not ou may not vez de cannot na mensagem Obsoleta.
  2. Contanto que a exceção permaneça, eu colocaria a mesma string em sua mensagem, apenas no caso de o método ser chamado por meio de um elenco ou genérico aberto.

@ Joe4evr Por mim tudo bem; Eu atualizei o comentário. Também pode ser benéfico incluir a mesma mensagem na exceção GetHashCode , então:

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

@morganbr Por que você reabriu isso?

O PR para expô-lo no CoreFX ainda não foi realizado.

@gimpf você tem o código que você

@JonHanna , gostaria de saber como estão os seus testes para que possamos começar a pensar sobre o que seria útil em uma API de hash não criptográfica de uso geral.

@morganbr Onde haveria um fórum apropriado para discutir tal API? Espero que essa API consista em mais do que apenas o menor denominador comum, e talvez uma boa API também precise de um tratamento de escrita JIT aprimorado de estruturas maiores. Discutindo tudo o que poderia ser melhor feito em uma edição separada ...

@gimpf Abriu um para você. dotnet / corefx # 25666

@morganbr - Podemos obter o nome e a versão do pacote que incluirá este commit?

@karelz , você pode ajudar @smitpatel com

Eu tentaria a compilação diária do .NET Core - esperaria até amanhã.
Não acho que exista um pacote do qual você possa simplesmente depender.

Pergunta para os participantes aqui. O Roslyn IDE permite aos usuários gerar um impl GetHashCode com base em um conjunto de campos / propriedades em sua classe / estrutura. Idealmente, as pessoas poderiam usar o novo HashCode.Combine que foi adicionado em https://github.com/dotnet/corefx/pull/25013 . No entanto, alguns usuários não terão acesso a esse código. Portanto, gostaríamos de ainda ser capazes de gerar um GetHashCode que funcione para eles.

Recentemente, percebemos que a forma que geramos é problemática. Ou seja, porque o VB compila com verificações de estouro ativadas por padrão, e nosso impl irá causar estouros. Além disso, o VB não tem como desabilitar as verificações de estouro para uma região do código. Pode ser ativado ou desativado inteiramente para toda a montagem.

Por isso, adoraria poder substituir o implemento que fornecemos por um formulário que não sofra com esses problemas. Idealmente, o formulário gerado teria as seguintes propriedades:

  1. Uma / duas linhas em GetHashCode por campo / propriedade usada.
  2. Sem transbordamento.
  3. Hashing razoavelmente bom. Não esperamos resultados surpreendentes. Mas algo que esperançosamente já foi examinado para ser decente e não ter os problemas que você costuma ter com a + b + c + d ou a ^ b ^ c ^ d .
  4. Sem dependências / requisitos adicionais no código.

Por exemplo, uma opção para VB seria gerar algo como:

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

Mas isso depende de ter uma referência a System.ValueTuple. Idealmente, poderíamos ter um implante que funcione mesmo na ausência dele.

Alguém sabe sobre um algoritmo de hash decente que pode funcionar com essas restrições? Obrigado!

-

Nota: nosso código emitido existente é:

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

Isso pode claramente transbordar.

Isso também não é um problema para C #, pois podemos apenas adicionar unchecked { } ao redor desse código. Esse controle refinado não é possível no VB.

Alguém sabe sobre um algoritmo de hash decente que pode funcionar com essas restrições? Obrigado!

Bem, você poderia fazer Tuple.Create(...).GetHashCode() . Obviamente, isso incorre em alocações, mas parece melhor do que lançar uma exceção.

Existe alguma razão pela qual você não pode simplesmente dizer ao usuário para instalar System.ValueTuple ? Como é um recurso de linguagem embutido, tenho certeza que o pacote System.ValueTuple é muito compatível com basicamente todas as plataformas, certo?

Obviamente, isso incorre em alocações, mas parece melhor do que lançar uma exceção.

sim. seria bom não ter que causar alocações.

Existe alguma razão pela qual você não pode simplesmente dizer ao usuário para instalar System.ValueTuple?

Esse seria o comportamento se gerarmos a abordagem ValueTuple. No entanto, novamente, seria bom se pudéssemos apenas gerar algo bom que se encaixasse na forma como o usuário estruturou seu código atualmente, sem fazê-lo alterar sua estrutura de uma forma pesada.

Realmente parece que os usuários VB deveriam ter uma maneira de resolver este problema de uma maneira razoável :) Mas essa abordagem está me escapando :)

@CyrusNajmabadi , Se você realmente precisa fazer seu próprio cálculo de hash no código do usuário, CRC32 pode funcionar, pois é uma combinação de pesquisas de tabela e XORs (mas não aritmética que pode estourar). No entanto, existem algumas desvantagens:

  1. CRC32 não tem grande entropia (mas provavelmente ainda é melhor do que o que Roslyn emite agora).
  2. Você precisaria colocar uma tabela de pesquisa de 256 entradas em algum lugar do código ou emitir código para gerar a tabela de pesquisa.

Se você ainda não estiver fazendo isso, espero que possa detectar o tipo HashCode e usá-lo quando possível, já que XXHash deve ser muito melhor.

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

Fazemos o seguinte:

  1. Use System.HashCode se estiver disponível. Feito.
  2. Caso contrário, se em C #:
    2a. Se não estiver no modo verificado: Gere hash não rolado.
    2b. Se estiver no modo marcado: Gera hash não rolado, agrupado em 'desmarcado {}'.
  3. Caso contrário, se em VB:
    3b. Se não estiver no modo verificado: Gere hash não rolado.
    3c. Se estiver no modo verificado, mas tiver acesso a System.ValueTuple: Gerar Return (a, b, c, ...).GetHashCode()
    3d. Se estiver no modo marcado, sem acesso a System.ValueTuple. Gere hash desenrolado, mas adicione um comentário em VB que estouros são muito prováveis.

É '3d' que é realmente lamentável. Basicamente, alguém usando VB, mas não usando ValueTuple ou um sistema recente, não será capaz de nos usar para obter um algoritmo hash razoável gerado para eles.

Você precisaria colocar uma tabela de pesquisa de 256 entradas em algum lugar do código

Isso seria completamente intragável :)

O código de geração de tabelas também é desagradável? Pelo menos seguindo o exemplo da Wikipedia , não é muito código (mas ainda tem que ir em algum lugar na fonte do usuário).

Quão terrível seria adicionar a origem HashCode ao projeto como Roslyn faz (com IL) com (o muito mais simples) definições de classe de atributo do compilador quando elas não estão disponíveis por meio de nenhum assembly referenciado?

Quão terrível seria adicionar a origem HashCode ao projeto como Roslyn faz com (o muito mais simples) definições de classe de atributo do compilador quando elas não estão disponíveis por meio de nenhum assembly referenciado?

  1. A fonte HashCode não precisa de comportamento de estouro?
  2. Eu dei uma olhada na fonte HashCode. É não trivial. Gerar toda essa gosma no projeto do usuário seria muito pesado.

Estou apenas surpreso de que não há boas maneiras de fazer com que a matemática do overflow funcione em VB :(

Portanto, no mínimo, mesmo se estivéssemos combinando dois valores, parece que teríamos que criar:

`` `c #
var hc1 = (uint) (valor1? .GetHashCode () ?? 0); // pode transbordar
var hc2 = (uint) (valor2? .GetHashCode () ?? 0); // pode transbordar

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

assim como QueueRound:

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

Então, honestamente, não vejo como isso funcionaria :(

Seria terrível adicionar o código-fonte HashCode ao projeto como Roslyn faz (com IL) com (muito

Como você vê isso funcionando? O que os clientes escreveriam e o que os compiladores fariam em resposta?

Além disso, algo que resolveria tudo isso é se .Net já tiver ajudantes públicos expostos na API de superfície que convertem de uint em int32 (e vice-versa) sem estouro.

Isso existe? Nesse caso, posso escrever facilmente as versões do VB, apenas usando-as para as situações em que precisamos ir entre os tipos sem transbordar.

O código de geração de tabelas também é desagradável?

Eu acho que sim. Quer dizer, pense nisso da perspectiva do cliente. Eles querem apenas um método GetHashCode decente que seja bem independente e forneça resultados razoáveis. Ter esse recurso e aumentar seu código com porcarias auxiliares vai ser bem desagradável. Também é muito ruim, visto que a experiência com C # vai funcionar bem.

Você pode conseguir obter aproximadamente o comportamento de estouro correto lançando de e para alguma combinação de tipos de 64 bits assinados e não assinados. Algo assim (não testado e não conheço a sintaxe de cast VB):

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

Como você sabe que o seguinte não transborda?

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

Ou o elenco final (int32) para esse assunto?

Eu não sabia que ele usaria operações de conversão verificadas por estouro. Acho que você pode mascarar até 32 bits antes de lançar:

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

presumivelmente 31 bits, como um valor de uint32.Max também estouraria na conversão para Int32 :)

Isso é definitivamente possível. Feio ... mas possível :) Pode haver muitos moldes neste código.

OK. Acho que tenho uma solução viável. O núcleo do algoritmo que geramos hoje é:

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

Digamos que estejamos fazendo matemática de 64 bits, mas "hashCode" foi limitado a 32 bits. Então <largest_32_bit> * -1521134295 + <largest_32_bit> não irá estourar 64 bits. Portanto, sempre podemos fazer as contas em 64 bits e, em seguida, reduzir para 32 (ou 32 bits) para garantir que a próxima rodada não transborde.

Obrigado!

@ MaStr11 @morganbr @sharwell e todos aqui. Eu atualizei meu código para gerar o seguinte 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)

Alguém pode me verificar a sanidade para ter certeza de que isso faz sentido e não deve transbordar, mesmo com o modo selecionado ativado?

@CyrusNajmabadi , que não estourará (porque Int64.Max = Int32.Max * Int32.Max e suas constantes são muito menores do que isso), mas você está mascarando o bit alto para zero, então é apenas um hash de 31 bits. Deixar a parte alta ativada é considerado um estouro?

@CyrusNajmabadi hashCode é um Long que pode estar em qualquer lugar de 0 a Integer.MaxValue . Por que estou recebendo isso?

image

Mas não, não pode realmente transbordar.

A propósito, prefiro que Roslyn adicione um pacote NuGet do que um hash abaixo do ideal.

mas você está mascarando o bit alto para zero, então é apenas um hash de 31 bits. Deixar a parte alta ativada é considerado um estouro?

Este é um bom ponto. Acho que estava pensando em outro algoritmo que estava usando uints. Portanto, para converter com segurança de longo em uint, eu precisei não incluir o bit de sinal. No entanto, como tudo isso é matemática com sinais, acho que seria bom apenas mascarar contra 0xffffffff garantindo que apenas manteremos os 32 bits inferiores após adicionar cada entrada.

Prefiro que Roslyn adicione um pacote NuGet do que adicionar um hash abaixo do ideal.

Os usuários já podem fazer isso se quiserem. Trata-se do que fazer quando os usuários não adicionam ou não podem adicionar essas dependências. Também se trata de fornecer um hash razoavelmente "bom o suficiente" para os usuários. ou seja, algo melhor do que a abordagem comum "x + y + z" que as pessoas costumam adotar. Não se destina a ser 'ideal' porque não existe uma boa definição do que é 'ideal' quando se trata de hash para todos os usuários. Observe que a abordagem que estamos adotando aqui é aquela já emitida pelo compilador para tipos anônimos. Ele exibe um comportamento razoavelmente bom, embora não adicione uma tonelada de complexidade ao código do usuário. Com o tempo, à medida que mais e mais usuários são capazes de avançar, isso pode desaparecer lentamente e ser substituído por HashCode.Combine para a maioria das pessoas.

Então, trabalhei um pouco nisso e cheguei ao seguinte que acho que aborda todas as preocupações:

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

A peça que é interessante é chamar especificamente .GetHashCode() no valor int64 produzido por (hashCode * -1521134295 + a.GetHashCode()) . Chamar .GetHashCode neste valor de 64 bits tem duas boas propriedades para nossas necessidades. Primeiro, ele garante que hashCode armazene apenas um valor int32 legal nele (o que torna a conversão final de retorno sempre segura para execução). Em segundo lugar, ele garante que não percamos nenhuma informação valiosa nos 32 bits superiores do valor temp int64 com o qual estamos trabalhando.

@CyrusNajmabadi Na verdade, oferecer para instalar o pacote é o que eu estava perguntando. Me salva de ter que fazer isso.

Se você digitar HashCode, então, se System.HashCode for fornecido em um pacote nuget MS, a Roslyn o oferecerá.

Eu quero gerar a sobrecarga GetHashCode inexistente e instalar o pacote na mesma operação.

Não acho que seja uma escolha apropriada para a maioria dos usuários. Adicionar dependências é uma operação muito pesada à qual os usuários não devem ser forçados. Os usuários podem decidir o momento certo para fazer essas escolhas e o IDE respeitará isso. Essa tem sido a abordagem que adotamos com todos os nossos recursos até agora, e tem sido uma abordagem saudável que as pessoas parecem gostar.

Nota: em qual pacote nuget esta api está sendo incluída para adicionarmos uma referência?

A implementação está em System.Private.CoreLib.dll, portanto, viria como parte do pacote de tempo de execução. O contrato é System.Runtime.dll.

OK. Se for esse o caso, então parece que um usuário obteria isso se / quando mudasse para um Target Framework mais recente. Esse tipo de coisa não é de forma alguma uma etapa que eu faria o "gerar igual + código hash" executar no projeto de um usuário.

Esta página foi útil?
0 / 5 - 0 avaliações