Di .NET, GetHashCode
metode ini digunakan di banyak tempat di seluruh pustaka kelas dasar .NET. Menerapkannya dengan benar sangat penting untuk menemukan item dengan cepat dalam koleksi atau ketika menentukan kesetaraan.
Apakah ada algoritma standar atau praktik terbaik tentang cara menerapkan GetHashCode
untuk kelas khusus saya sehingga saya tidak menurunkan kinerja?
.net
algorithm
hashcode
gethashcode
bitbonk
sumber
sumber
GetHashCode
. Saya berharap ini akan bermanfaat bagi orang lain. Pedoman dan aturan untuk GetHashCode ditulis oleh Eric LippertGetHashCode()
digunakan dalam implementasi yang sangat banyakEquals()
. Itulah yang saya maksud dengan pernyataan itu.GetHashCode()
di dalamEquals()
sering digunakan sebagai jalan pintas untuk menentukan ketidaksetaraan , karena jika dua objek memiliki kode hash yang berbeda mereka harus menjadi objek yang tidak sama dan sisanya dari pemeriksaan kesetaraan tidak harus dieksekusi.GetHashCode()
danEquals()
perlu melihat semua bidang kedua objek (Persamaan harus melakukan ini jika kode hash sama atau tidak-dicentang). Karena itu, panggilan keGetHashCode()
dalamEquals()
seringkali berlebihan dan dapat mengurangi kinerja.Equals()
mungkin juga dapat melakukan hubungan pendek, membuatnya lebih cepat - namun dalam beberapa kasus kode hash mungkin di-cache, membuatGetHashCode()
pemeriksaan lebih cepat dan sangat bermanfaat. Lihat pertanyaan ini untuk lebih lanjut.Jawaban:
Saya biasanya pergi dengan sesuatu seperti implementasi yang diberikan di Jawa Efektif luar biasa Josh Bloch . Ini cepat dan menciptakan hash yang cukup bagus yang tidak mungkin menyebabkan tabrakan. Pilih dua bilangan prima yang berbeda, mis. 17 dan 23, dan lakukan:
Seperti disebutkan dalam komentar, Anda mungkin lebih baik memilih bilangan prima besar untuk dikalikan dengan sebagai gantinya. Tampaknya 486187739 baik ... dan meskipun sebagian besar contoh yang saya lihat dengan angka kecil cenderung menggunakan bilangan prima, setidaknya ada algoritma yang sama di mana angka non-prime sering digunakan. Dalam contoh yang tidak cukup- FNV nanti, misalnya, saya telah menggunakan angka yang tampaknya bekerja dengan baik - tetapi nilai awal bukan yang utama. (Konstanta pengali adalah prima. Saya tidak tahu seberapa penting itu.)
Ini lebih baik daripada praktik umum
XOR
kode hash karena dua alasan utama. Misalkan kita memiliki tipe dengan duaint
bidang:Omong-omong, algoritma sebelumnya adalah yang saat ini digunakan oleh kompiler C # untuk tipe anonim.
Halaman ini memberikan beberapa opsi. Saya pikir untuk sebagian besar kasus di atas adalah "cukup baik" dan sangat mudah untuk diingat dan diperbaiki. The FNV alternatif adalah sama sederhana, tetapi menggunakan konstanta yang berbeda dan
XOR
bukanADD
sebagai operasi menggabungkan. Ini terlihat sesuatu seperti kode di bawah, tetapi algoritma FNV yang normal beroperasi pada byte individu, jadi ini akan membutuhkan memodifikasi untuk melakukan satu iterasi per byte, bukan per nilai hash 32-bit. FNV juga dirancang untuk panjang data variabel, sedangkan cara kita menggunakannya di sini selalu untuk jumlah nilai bidang yang sama. Komentar pada jawaban ini menunjukkan bahwa kode di sini tidak benar-benar berfungsi dengan baik (dalam contoh kasus diuji) sebagai pendekatan tambahan di atas.Perhatikan bahwa satu hal yang perlu diperhatikan adalah bahwa idealnya Anda harus mencegah negara Anda yang sensitif terhadap kesetaraan (dan dengan demikian memiliki kode hash) berubah setelah menambahkannya ke koleksi yang bergantung pada kode hash.
Sesuai dokumentasi :
sumber
Dictionary<TKey,TValue>
mengasumsikan modulo distribusi prima baik. Dan 23 adalah salah satunya. Jadi jika Anda memiliki kamus dengan Kapasitas 23 hanya kontribusi terakhir untukGetHashCode
memengaruhi kode hash gabungan. Jadi saya lebih suka menggunakan 29 daripada 23.null
- yang tidak sama dengan mengabaikan bidang.Jenis Anonim
Microsoft sudah menyediakan generator HashCode generik yang baik: Cukup salin nilai properti / bidang Anda ke jenis anonim dan hash:
Ini akan berfungsi untuk sejumlah properti. Itu tidak menggunakan tinju. Itu hanya menggunakan algoritma yang sudah diterapkan dalam kerangka kerja untuk jenis anonim.
ValueTuple - Pembaruan untuk C # 7
Seperti @cactuaroid menyebutkan dalam komentar, tuple nilai dapat digunakan. Ini menghemat beberapa penekanan tombol dan yang lebih penting dijalankan secara murni di stack (tidak ada Sampah):
(Catatan: Teknik asli menggunakan jenis anonim tampaknya membuat objek pada heap, yaitu sampah, karena jenis anonim diimplementasikan sebagai kelas, meskipun ini mungkin dioptimalkan oleh kompiler. Akan menarik untuk membandingkan opsi ini, tetapi Opsi tuple harus lebih unggul.)
sumber
GetHashCode
implementasi anonim sangat efektif (BTW itu sama dengan yang ada di jawaban Jon Skeet), tetapi satu-satunya masalah dengan solusi ini adalah Anda menghasilkan instance baru padaGetHashCode
panggilan apa pun . Ini bisa menjadi sedikit overhead-ish khususnya dalam hal akses intensif ke koleksi hash besar ...new { PropA, PropB, PropC, PropD }.GetHashCode()
juga mengatakanNew With {Key PropA}.GetHashCode()
Jika tidak GetHashCode tidak akan mengembalikan kode hash yang sama untuk objek yang berbeda dengan properti 'pengidentifikasi' yang sama.Inilah pembantu kode hash saya.
Keuntungannya adalah ia menggunakan argumen tipe umum dan karenanya tidak akan menyebabkan tinju:
Juga memiliki metode ekstensi untuk menyediakan antarmuka yang lancar, sehingga Anda dapat menggunakannya seperti ini:
atau seperti ini:
sumber
T[]
terpisah karena sudahIEnumerable<T>
Saya memiliki kelas Hashing di perpustakaan Helper yang saya gunakan untuk tujuan ini.
Kemudian, cukup Anda dapat menggunakannya sebagai:
Saya tidak menilai kinerjanya, jadi ada umpan balik yang disambut.
sumber
unchecked
adalah untuk menghindari pengecualian pada overflow yang diinginkan padaGetHashCode
. Jadi tidak salah kalau nilainya melimpahint
dan tidak ada salahnya sama sekali.null
dilewati sepenuhnya dapat memberikan hasil yang tidak terduga. Alih-alih melewatkannya, Anda harus menggunakan beberapa nilai konstan alih-alihinput[i].GetHashCode()
ketikainput[i]
bernilai nol.Inilah kelas pembantu saya menggunakan implementasi Jon Skeet .
Pemakaian:
Jika Anda ingin menghindari menulis metode ekstensi untuk System.Int32:
Itu masih menghindari alokasi tumpukan dan digunakan dengan cara yang persis sama:
Sunting (Mei 2018):
EqualityComparer<T>.Default
pengambil sekarang adalah intrinsik JIT - permintaan tarik disebutkan oleh Stephen Toub dalam posting blog ini .sumber
var h = Equals(obj, default(T)) ? 0 : obj.GetHashCode();
obj != null
akan mengkompilasibox
instruksi yang akan mengalokasikan memori jikaT
merupakan tipe nilai. Sebagai gantinya Anda dapat menggunakanobj.Equals(null)
yang akan dikompilasi ke panggilan virtualEquals
metode ini.this.hashCode != h
. Itu tidak akan mengembalikan nilai yang sama..NET Standard 2.1 Dan Di Atas
Jika Anda menggunakan .NET Standard 2.1 atau di atasnya, Anda dapat menggunakan struct System.HashCode . Ada dua metode untuk menggunakannya:
HashCode.Combine
The
Combine
metode dapat digunakan untuk membuat kode hash, mengingat hingga delapan objek.HashCode.Add
The
Add
metode membantu Anda untuk berurusan dengan koleksi:GetHashCode Menjadi Mudah
Anda dapat membaca posting blog lengkap ' GetHashCode Made Easy ' untuk rincian dan komentar lebih lanjut.
Contoh Penggunaan
Penerapan
Apa yang Membuat Algoritma Baik?
Kecepatan
Algoritma yang menghitung kode hash harus cepat. Algoritma sederhana biasanya akan menjadi yang lebih cepat.
Deterministik
Algoritma hashing perlu deterministik yaitu diberi input yang sama harus selalu menghasilkan output yang sama.
Kurangi Tabrakan
Algoritma yang menghitung kode hash perlu menjaga tabrakan hash ke minumum. Tabrakan hash adalah situasi yang terjadi ketika dua panggilan ke
GetHashCode
dua objek yang berbeda menghasilkan kode hash yang identik. Perhatikan bahwa tabrakan diperbolehkan (beberapa memiliki kesalahpahaman bahwa mereka tidak) tetapi mereka harus dijaga agar tetap minimum.Fungsi hash yang baik harus memetakan input yang diharapkan serata mungkin pada rentang outputnya. Itu harus memiliki keseragaman.
Mencegah DoS
Di .NET Core setiap kali Anda me-restart aplikasi, Anda akan mendapatkan kode hash yang berbeda. Ini adalah fitur keamanan untuk mencegah serangan Denial of Service (DoS). Untuk .NET Framework Anda harus mengaktifkan fitur ini dengan menambahkan file App.config berikut:
Karena fitur ini, kode hash tidak boleh digunakan di luar domain aplikasi tempat kode itu dibuat, kode hash tidak boleh digunakan sebagai bidang kunci dalam koleksi dan kode tersebut tidak boleh bertahan.
Baca lebih lanjut tentang ini di sini .
Aman Secara Kriptografis?
Algoritme tidak harus berupa fungsi hash Kriptografis . Artinya tidak harus memenuhi ketentuan berikut:
sumber
Dalam kebanyakan kasus di mana Equals () membandingkan beberapa bidang, tidak masalah jika GetHash () Anda hash pada satu bidang atau banyak. Anda hanya perlu memastikan bahwa menghitung hash benar-benar murah ( Tidak ada alokasi , tolong) dan cepat ( Tidak ada perhitungan berat dan tentu saja tidak ada koneksi database) dan menyediakan distribusi yang baik.
Angkat berat harus menjadi bagian dari metode Equals (); hash harus menjadi operasi yang sangat murah untuk memungkinkan memanggil Persamaan () pada item sesedikit mungkin.
Dan satu tip terakhir: Jangan mengandalkan GetHashCode () yang stabil di atas beberapa aplikasi berjalan . Banyak tipe .Net tidak menjamin kode hash mereka untuk tetap sama setelah restart, jadi Anda hanya harus menggunakan nilai GetHashCode () untuk dalam struktur data memori.
sumber
GetHashCode
melakukan alokasi memori, asalkan itu hanya melakukannya pertama kali digunakan (dengan doa berikutnya hanya mengembalikan hasil cache). Yang penting bukanlah bahwa seseorang harus berusaha keras untuk menghindari tabrakan, melainkan bahwa ia harus menghindari tabrakan "sistemik". Jika suatu tipe memiliki duaint
bidangoldX
dannewX
yang sering berbeda satu, nilai hasholdX^newX
akan menetapkan 90% dari nilai hash rekaman tersebut dari 1, 2, 4, atau 8. MenggunakanoldX+newX
[hitung aritmatika] dapat menghasilkan lebih banyak tabrakan ...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:
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:
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:
* 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 bahwaGetHashCode()
memperlakukan sendiri presisi sebagai signifikan sedangkan miliknyaEquals()
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 daripadastring.GetHashCode()
pada 64 bit, yang jauh lebih cepat daripada SpookyHash pada 32 bit, meskipun masih cukup cepat menjadi pilihan yang masuk akal.sumber
long
nilai untuk hasil antara, dan kemudian mengurangkan hasil akhir menjadi sebuahint
. Apakah itu sepertinya ide yang bagus? Kekhawatiran saya adalah bahwa seseorang menggunakan mis hash = (hash * 31) + nextField, maka pasangan nilai-nilai yang cocok hanya akan mempengaruhi 27 bit atas dari hash. Membiarkan perhitungan meluas kelong
dan membungkus barang-barang akan meminimalkan bahaya itu..Update()
dengan beberapa nilai sesuai jawaban di atas akan melakukan trik.Ini bagus:
Dan inilah cara menggunakannya:
sumber
GetHashCode()
metode, jadi Anda selalu dapat menggunakan metode denganparams
parameter array. Atau saya kehilangan sesuatu di sini?h += (h << 10); h ^= (h >> 6); h += (h << 3); h ^= (h >> 11); h += (h << 15);
memiliki codesmell: mereka tidak bergantung pada salah input dan terlihat sangat membazir dengan saya.Pada https://github.com/dotnet/coreclr/pull/14863 , ada cara baru untuk menghasilkan kode hash yang super sederhana! Cukup tulis
Ini akan menghasilkan kode hash yang berkualitas tanpa Anda harus khawatir tentang detail implementasi.
sumber
HashCode
perubahan untuk corefx digabung hanya beberapa jam sebelum komentar Anda :) Tipe ini dijadwalkan untuk dikirimkan dalam .NET Core 2.1.Berikut ini adalah implementasi lain dari algoritma yang diposting di atas oleh Jon Skeet , tetapi tidak menyertakan alokasi atau operasi tinju:
Pemakaian:
Compiler akan memastikan
HashValue
tidak dipanggil dengan kelas karena batasan tipe generik. Tetapi tidak ada dukungan kompiler untukHashObject
karena menambahkan argumen generik juga menambahkan operasi tinju.sumber
Ini pendekatan saya yang sederhana. Saya menggunakan pola pembangun klasik untuk ini. Ini adalah typesafe (tanpa tinju / unboxing) dan juga kompatibel dengan .NET 2.0 (tidak ada metode ekstensi dll.).
Digunakan seperti ini:
Dan ini adalah kelas pembangun acutal:
sumber
AddItems<T>(params T[] items)
metode lebih sering di kelas helper (daripada meneleponAddItem(T)
setiap kali).this.result * Prime2 * item.GetHashCode()
ketika sering digunakanthis.result * Prime2 + item.GetHashCode()
?AddItems<T>(params T[] items)
lebih sering karenatypeof(T1) != typeof(T2)
dll.Pengguna ReSharper dapat membuat GetHashCode, Equals, dan lainnya dengan
ReSharper -> Edit -> Generate Code -> Equality Members
.sumber
Jika kita memiliki tidak lebih dari 8 properti (semoga), berikut adalah alternatif lain.
ValueTuple
adalah struct dan tampaknya memilikiGetHashCode
implementasi yang solid .Itu berarti kita bisa melakukan ini:
Mari kita lihat implementasi NET Core saat ini untuk
ValueTuple
'sGetHashCode
.Ini dari
ValueTuple
:Dan ini dari
HashHelper
:Dalam Bahasa Inggris:
Akan menyenangkan untuk mengetahui lebih banyak tentang properti dari algoritma hash code ROL-5 ini.
Sayangnya, menunda
ValueTuple
untuk kita sendiriGetHashCode
mungkin tidak secepat yang kita inginkan dan harapkan. Komentar ini dalam diskusi terkait menggambarkan bahwa panggilan langsungHashHelpers.Combine
lebih berprestasi. Di sisi lain, itu internal, jadi kita harus menyalin kode, mengorbankan banyak dari apa yang kita dapatkan di sini. Selain itu, kami akan bertanggung jawab untuk mengingat terlebih dahuluCombine
dengan benih acak. Saya tidak tahu apa konsekuensinya jika kita melewatkan langkah itu.sumber
h1 >> 27
0 untuk mengabaikannya,h1 << 5
sama denganh1 * 32
itu sama denganh1 * 33 ^ h2
. Menurut halaman ini , itu disebut "Modified Bernstein".Sebagian besar pekerjaan saya dilakukan dengan konektivitas database yang berarti bahwa semua kelas saya memiliki pengidentifikasi unik dari database. Saya selalu menggunakan ID dari database untuk menghasilkan kode hash.
sumber
_id.GetHashCode
karena maksudnya jelas.Cukup mirip dengan solusi nightcoder kecuali lebih mudah untuk meningkatkan bilangan prima jika Anda mau.
PS: Ini adalah salah satu saat di mana Anda muntah sedikit di mulut, mengetahui bahwa ini dapat di refactored menjadi satu metode dengan 9 metode baku tetapi akan lebih lambat, jadi Anda hanya menutup mata dan mencoba melupakannya.
sumber
Saya mengalami masalah dengan mengapung dan desimal menggunakan implementasi yang dipilih sebagai jawaban di atas.
Tes ini gagal (mengapung; hash sama meskipun saya mengubah 2 nilai menjadi negatif):
Tetapi tes ini lolos (dengan int):
Saya mengubah implementasi saya untuk tidak menggunakan GetHashCode untuk tipe primitif dan sepertinya berfungsi lebih baik
sumber
unchecked
TIDAK mempengaruhiConvert.ToInt32
:uint
,long
,float
,double
dandecimal
semua bisa meluap di sini.Microsoft memimpin untuk beberapa cara ...
Saya bisa menebak bahwa untuk beberapa int besar Anda dapat menggunakan ini:
Dan sama untuk multi-type: semua dikonversi terlebih dahulu untuk
int
digunakanGetHashCode()
maka nilai int akan di-xor'ed dan hasilnya adalah hash Anda.Bagi mereka yang menggunakan hash sebagai ID (maksud saya nilai unik), hash secara alami terbatas pada sejumlah digit, saya pikir itu 5 byte untuk algoritma hashing, setidaknya MD5.
Anda dapat mengubah beberapa nilai menjadi nilai hash dan beberapa di antaranya sama, jadi jangan gunakan itu sebagai pengidentifikasi. (mungkin suatu hari nanti saya akan menggunakan komponen Anda)
sumber
Ini adalah kelas pembantu statis yang mengimplementasikan implementasi Josh Bloch; dan memberikan kelebihan secara eksplisit untuk "mencegah" tinju, dan juga untuk mengimplementasikan hash khusus untuk primitif panjang.
Anda dapat melewati perbandingan string yang cocok dengan implementasi yang sama dengan Anda.
Karena output Hash selalu merupakan int, Anda hanya dapat membuat panggilan Hash.
sumber
HashKeysAndValues
Metode telah diperbaiki: itu memanggilHashKeyAndValue
.Jika Anda ingin melakukan polyfill
HashCode
darinetstandard2.1
Catatan: Jika digunakan dengan
struct
, itu akan mengalokasikan memori karena tinjusumber