Sampai baru-baru ini jawaban saya akan sangat dekat dengan Jon Skeet di sini. Namun, saya baru-baru ini memulai sebuah proyek yang menggunakan tabel hash power-of-two, yaitu tabel hash di mana ukuran tabel internal adalah 8, 16, 32, dll. Ada alasan bagus untuk memilih ukuran bilangan prima, tetapi ada ada beberapa kelebihan pada power-of-two size juga.
Dan itu cukup menyebalkan. Jadi setelah sedikit percobaan dan penelitian, saya mulai mem-hashing hash saya dengan yang berikut:
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);
}
}
Dan kemudian tabel hash power-of-two saya tidak lagi menyedot.
Ini mengganggu saya, karena hal di atas seharusnya tidak berfungsi. Atau lebih tepatnya, itu tidak akan berfungsi kecuali yang asli GetHashCode()
buruk dengan cara yang sangat khusus.
Mencampur ulang kode hash tidak dapat meningkatkan kode hash yang hebat, karena satu-satunya efek yang mungkin adalah bahwa kami memperkenalkan beberapa tabrakan lagi.
Mencampurkan kembali kode hash tidak dapat meningkatkan kode hash yang mengerikan, karena satu-satunya efek yang mungkin adalah kita mengubah misalnya sejumlah besar tabrakan pada nilai 53 ke sejumlah besar nilai 18,3487.291.
Mencampurkan kembali kode hash hanya dapat meningkatkan kode hash yang melakukan setidaknya cukup baik dalam menghindari tabrakan absolut sepanjang rentangnya (2 32 nilai yang mungkin) tetapi sangat buruk dalam menghindari tabrakan ketika modulo down untuk penggunaan aktual dalam tabel hash. Sementara modulo sederhana dari tabel power-of-two membuat ini lebih jelas, itu juga memiliki efek negatif dengan tabel bilangan prima yang lebih umum, yang tidak begitu jelas (kerja ekstra dalam pengulangan akan lebih besar daripada manfaatnya , tetapi manfaatnya tetap ada).
Sunting: Saya juga menggunakan pengalamatan terbuka, yang juga akan meningkatkan sensitivitas terhadap tabrakan, mungkin lebih daripada fakta bahwa itu adalah kekuatan dua.
Dan yah, itu mengganggu berapa banyak string.GetHashCode()
implementasi di .NET (atau belajar di sini ) dapat ditingkatkan dengan cara ini (pada urutan tes berjalan sekitar 20-30 kali lebih cepat karena lebih sedikit tabrakan) dan lebih mengganggu berapa banyak kode hash saya sendiri dapat ditingkatkan (lebih dari itu).
Semua implementasi GetHashCode () yang saya kodekan di masa lalu, dan memang digunakan sebagai dasar jawaban di situs ini, jauh lebih buruk daripada yang saya bayangkan . Sebagian besar waktu itu "cukup baik" untuk banyak kegunaan, tetapi saya menginginkan sesuatu yang lebih baik.
Jadi saya meletakkan proyek itu di satu sisi (itu adalah proyek kesayangan) dan mulai mencari cara untuk menghasilkan kode hash yang baik dan didistribusikan dengan baik di .NET dengan cepat.
Pada akhirnya saya memutuskan untuk memindahkan SpookyHash ke .NET. Memang kode di atas adalah versi jalur cepat menggunakan SpookyHash untuk menghasilkan output 32-bit dari input 32-bit.
Sekarang, SpookyHash bukanlah cepat untuk mengingat sepotong kode. Port saya lebih kurang karena saya menggunakan banyak itu untuk kecepatan yang lebih baik *. Tapi untuk itulah penggunaan kembali kode.
Lalu aku menaruh bahwa proyek ke satu sisi, karena seperti proyek asli telah menghasilkan pertanyaan tentang bagaimana untuk menghasilkan kode hash yang lebih baik, sehingga proyek yang menghasilkan pertanyaan tentang bagaimana untuk menghasilkan yang lebih baik NET memcpy.
Kemudian saya kembali, dan menghasilkan banyak kelebihan untuk dengan mudah memberi makan hampir semua jenis asli (kecuali decimal
†) ke dalam kode hash.
Ini cepat, di mana Bob Jenkins layak mendapatkan sebagian besar kredit karena kode aslinya yang saya porting masih lebih cepat, terutama pada mesin 64-bit yang algoritmanya dioptimalkan untuk ‡.
Kode lengkap dapat dilihat di https://bitbucket.org/JonHanna/spookilysharp/src tetapi pertimbangkan bahwa kode di atas adalah versi yang disederhanakan.
Namun, karena sekarang sudah ditulis, orang dapat menggunakannya dengan lebih mudah:
public override int GetHashCode()
{
var hash = new SpookyHash();
hash.Update(field1);
hash.Update(field2);
hash.Update(field3);
return hash.Final().GetHashCode();
}
Ini juga membutuhkan nilai seed, jadi jika Anda perlu berurusan dengan input yang tidak dipercaya dan ingin melindungi terhadap serangan Hash DoS, Anda dapat mengatur seed berdasarkan waktu kerja atau sejenisnya, dan membuat hasilnya tidak dapat diprediksi oleh penyerang:
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();
}
* Kejutan besar dalam hal ini adalah dengan menggunakan metode rotasi yang mengembalikan (x << n) | (x >> -n)
hal-hal yang ditingkatkan. Saya akan yakin bahwa jitter akan menjelaskan itu untuk saya, tetapi profiling menunjukkan sebaliknya.
† decimal
bukan asli dari perspektif .NET meskipun berasal dari C #. Masalah dengan itu adalah bahwa GetHashCode()
memperlakukan sendiri presisi sebagai signifikan sedangkan miliknya Equals()
tidak. Keduanya merupakan pilihan yang valid, tetapi tidak tercampur seperti itu. Dalam mengimplementasikan versi Anda sendiri, Anda harus memilih untuk melakukan satu, atau yang lain, tetapi saya tidak tahu yang Anda inginkan.
‡ Sebagai perbandingan. Jika digunakan pada string, SpookyHash pada 64 bit jauh lebih cepat daripada string.GetHashCode()
pada 32 bit yang sedikit lebih cepat daripada string.GetHashCode()
pada 64 bit, yang jauh lebih cepat daripada SpookyHash pada 32 bit, meskipun masih cukup cepat menjadi pilihan yang masuk akal.
GetHashCode
. Saya berharap ini akan bermanfaat bagi orang lain. Pedoman dan aturan untuk GetHashCode ditulis oleh Eric Lippert