Até recentemente, minha resposta teria sido muito próxima da de Jon Skeet aqui. No entanto, iniciei recentemente um projeto que usava tabelas de hash com duas potências, ou seja, tabelas em que o tamanho da tabela interna é 8, 16, 32 etc. Há uma boa razão para favorecer tamanhos de números primos, mas existem Existem também algumas vantagens para tamanhos de dois em dois.
E é muito ruim. Então, depois de um pouco de experimentação e pesquisa, comecei a re-misturar meus hashes com o seguinte:
public static int ReHash(int source)
{
unchecked
{
ulong c = 0xDEADBEEFDEADBEEF + (ulong)source;
ulong d = 0xE2ADBEEFDEADBEEF ^ c;
ulong a = d += c = c << 15 | c >> -15;
ulong b = a += d = d << 52 | d >> -52;
c ^= b += a = a << 26 | a >> -26;
d ^= c += b = b << 51 | b >> -51;
a ^= d += c = c << 28 | c >> -28;
b ^= a += d = d << 9 | d >> -9;
c ^= b += a = a << 47 | a >> -47;
d ^= c += b << 54 | b >> -54;
a ^= d += c << 32 | c >> 32;
a += d << 25 | d >> -25;
return (int)(a >> 1);
}
}
E então minha tabela de hash de duas potências não foi mais uma droga.
Isso me perturbou, porque o acima não deveria funcionar. Ou, mais precisamente, não deve funcionar, a menos que o original GetHashCode()
seja ruim de uma maneira muito particular.
Re-misturar um código de hash não pode melhorar um ótimo código de hash, porque o único efeito possível é que introduzimos mais algumas colisões.
A mistura de um código hash não pode melhorar um código hash terrível, porque o único efeito possível é alterar, por exemplo, um grande número de colisões no valor 53 para um grande número de valor 18,3487,291.
Misturar novamente um código de hash pode melhorar apenas um código de hash que se saiu pelo menos razoavelmente bem em evitar colisões absolutas em todo o seu intervalo (2 32 valores possíveis), mas muito mal em evitar colisões quando modulado para uso real em uma tabela de hash. Enquanto o módulo mais simples de uma tabela de potências de dois tornava isso mais aparente, também estava tendo um efeito negativo com as tabelas de números primos mais comuns, que não eram tão óbvias (o trabalho extra na reformulação superaria o benefício , mas o benefício ainda estaria lá).
Edit: Eu também estava usando o endereço aberto, o que também teria aumentado a sensibilidade à colisão, talvez mais do que o fato de ser uma potência de dois.
E bem, era perturbador o quanto as string.GetHashCode()
implementações no .NET (ou estudo aqui ) poderiam ser aprimoradas dessa maneira (na ordem dos testes executados cerca de 20 a 30 vezes mais rápidas devido a menos colisões) e mais perturbador quanto meus próprios códigos de hash poderia ser melhorado (muito mais que isso).
Todas as implementações GetHashCode () que eu codifiquei no passado e, de fato, usei como base de respostas neste site, foram muito piores do que eu havia passado . Na maioria das vezes era "bom o suficiente" para muitos usos, mas eu queria algo melhor.
Então, coloquei esse projeto de lado (de qualquer maneira, era um projeto para animais de estimação) e comecei a analisar como produzir rapidamente um bom código de hash bem distribuído no .NET.
No final, resolvi portar o SpookyHash para o .NET. Na verdade, o código acima é uma versão rápida do uso do SpookyHash para produzir uma saída de 32 bits a partir de uma entrada de 32 bits.
Agora, o SpookyHash não é um bom código rápido para lembrar. Meu porto é ainda menos, porque eu escrevi muito sobre ele para obter uma velocidade melhor *. Mas é para isso que serve a reutilização de código.
Depois, coloquei esse projeto de lado, porque, assim como o projeto original havia produzido a questão de como produzir um código hash melhor, esse projeto também produzia a questão de como produzir um melhor memcpy .NET.
Voltei e produzi muitas sobrecargas para alimentar facilmente quase todos os tipos nativos (exceto decimal
†) em um código hash.
É rápido, pelo qual Bob Jenkins merece a maior parte do crédito, porque seu código original de onde eu carreguei é ainda mais rápido, especialmente em máquinas de 64 bits para as quais o algoritmo é otimizado.
O código completo pode ser visto em https://bitbucket.org/JonHanna/spookilysharp/src, mas considere que o código acima é uma versão simplificada dele.
No entanto, como já está escrito, é possível usá-lo com mais facilidade:
public override int GetHashCode()
{
var hash = new SpookyHash();
hash.Update(field1);
hash.Update(field2);
hash.Update(field3);
return hash.Final().GetHashCode();
}
Ele também aceita valores de propagação, portanto, se você precisar lidar com informações não confiáveis e desejar proteger contra ataques Hash DoS, poderá definir uma propagação com base no tempo de atividade ou similar e tornar os resultados imprevisíveis pelos invasores:
private static long hashSeed0 = Environment.TickCount;
private static long hashSeed1 = DateTime.Now.Ticks;
public override int GetHashCode()
{
//produce different hashes ever time this application is restarted
//but remain consistent in each run, so attackers have a harder time
//DoSing the hash tables.
var hash = new SpookyHash(hashSeed0, hashSeed1);
hash.Update(field1);
hash.Update(field2);
hash.Update(field3);
return hash.Final().GetHashCode();
}
* Uma grande surpresa nisso é a introdução manual de um método de rotação que retornava (x << n) | (x >> -n)
itens aprimorados. Eu teria certeza de que o jitter teria indicado isso para mim, mas a criação de perfil mostrou o contrário.
† decimal
não é nativo da perspectiva .NET, embora seja do C #. O problema é que o próprio GetHashCode()
trata a precisão como significativa, enquanto o próprio Equals()
não. Ambos são escolhas válidas, mas não misturadas assim. Ao implementar sua própria versão, você precisa escolher uma ou outra, mas não sei o que você deseja.
‡ Como comparação. Se usado em uma string, o SpookyHash em 64 bits é consideravelmente mais rápido do que string.GetHashCode()
em 32 bits, um pouco mais rápido que string.GetHashCode()
em 64 bits, que é consideravelmente mais rápido que o SpookyHash em 32 bits, mas ainda rápido o suficiente para ser uma escolha razoável.
GetHashCode
. Espero que seja útil para os outros. Diretrizes e regras para o GetHashCode escritas por Eric Lippert