Saya ingin menyimpan beberapa lokasi piksel tanpa mengizinkan duplikat, jadi hal pertama yang terlintas dalam pikiran adalah HashSet<Point>
kelas yang serupa. Namun ini sepertinya sangat lambat dibandingkan dengan sesuatu seperti HashSet<string>
.
Misalnya, kode ini:
HashSet<Point> points = new HashSet<Point>();
using (Bitmap img = new Bitmap(1000, 1000))
{
for (int x = 0; x < img.Width; x++)
{
for (int y = 0; y < img.Height; y++)
{
points.Add(new Point(x, y));
}
}
}
membutuhkan waktu sekitar 22,5 detik.
Sementara kode berikut (yang bukan pilihan yang baik karena alasan yang jelas) hanya membutuhkan 1,6 detik:
HashSet<string> points = new HashSet<string>();
using (Bitmap img = new Bitmap(1000, 1000))
{
for (int x = 0; x < img.Width; x++)
{
for (int y = 0; y < img.Height; y++)
{
points.Add(x + "," + y);
}
}
}
Jadi, pertanyaan saya adalah:
- Apakah ada alasan untuk itu? Saya memeriksa jawaban ini , tetapi 22,5 detik jauh lebih banyak daripada angka yang ditunjukkan dalam jawaban itu.
- Apakah ada cara yang lebih baik untuk menyimpan poin tanpa duplikat?
c#
.net
performance
collections
hashset
Ahmed Abdelhameed
sumber
sumber
Jawaban:
Ada dua masalah kinerja yang disebabkan oleh struct Point. Sesuatu yang dapat Anda lihat ketika Anda menambah
Console.WriteLine(GC.CollectionCount(0));
kode tes. Anda akan melihat bahwa tes Point membutuhkan ~ 3720 koleksi tetapi tes string hanya membutuhkan ~ 18 koleksi. Tidak gratis. Ketika Anda melihat tipe nilai menginduksi begitu banyak koleksi maka Anda perlu menyimpulkan "uh-oh, terlalu banyak tinju".Yang menjadi masalah adalah bahwa
HashSet<T>
perluIEqualityComparer<T>
untuk menyelesaikan tugasnya. Karena Anda tidak menyediakan satu, itu harus kembali ke satu dikembalikan olehEqualityComparer.Default<T>()
. Metode itu dapat melakukan pekerjaan yang baik untuk string, itu mengimplementasikan IEquatable. Tetapi tidak untuk Point, ini adalah tipe yang berasal dari .NET 1.0 dan tidak pernah mendapatkan cinta generik. Yang bisa dilakukan hanyalah menggunakan metode Object.Masalah lainnya adalah bahwa Point.GetHashCode () tidak melakukan pekerjaan bintang dalam tes ini, terlalu banyak tabrakan, sehingga memalu Object.Equals () cukup banyak. String memiliki implementasi GetHashCode yang sangat baik.
Anda dapat menyelesaikan kedua masalah dengan menyediakan HashSet dengan pembanding yang baik. Seperti yang ini:
Dan gunakan itu:
Dan sekarang sekitar 150 kali lebih cepat, dengan mudah mengalahkan tes string.
sumber
obj.X << 16 | obj.Y;
implementasi khusus .|
. Untuk 3 angka, masuk akal untuk menggunakan 22 dan 11 sebagai shift. Untuk 4 angka akan menjadi 24, 16, 8. Namun masih akan ada tabrakan tetapi hanya jika jumlahnya menjadi besar. Tetapi juga sangat tergantung padaHashSet
implementasinya. Jika menggunakan open-adressing dengan "bit truncation" (saya rasa tidak!) Pendekatan shift kiri mungkin buruk.Alasan utama untuk penurunan kinerja adalah semua tinju terjadi (seperti yang sudah dijelaskan dalam jawaban Hans Passant ).
Terlepas dari itu, algoritma kode hash memperburuk masalah, karena menyebabkan lebih banyak panggilan
Equals(object obj)
sehingga meningkatkan jumlah konversi tinju.Perhatikan juga bahwa kode hash
Point
dihitung olehx ^ y
. Ini menghasilkan dispersi yang sangat sedikit dalam rentang data Anda, dan oleh karena itu ember dariHashSet
populasi berlebih - sesuatu yang tidak terjadistring
, di mana dispersi hash jauh lebih besar.Anda dapat memecahkan masalah itu dengan mengimplementasikan
Point
struct Anda sendiri (sepele) dan menggunakan algoritma hash yang lebih baik untuk rentang data yang Anda harapkan, misalnya dengan menggeser koordinat:Untuk beberapa saran yang baik ketika datang ke kode hash, baca posting blog Eric Lippert pada subjek .
sumber
GetHashCode
perform:unchecked(x ^ y)
sementara untukstring
itu terlihat jauh lebih rumit ..HashSet<long>()
, dan digunakanlist.Add(unchecked(x ^ y));
untuk menambahkan nilai ke HashSet. Ini sebenarnya bahkan lebih cepat daripadaHashSet<string>
(345 ms) . Apakah ini berbeda dari yang Anda gambarkan?list
ketika Anda selesai mengisinya?point
,HashSet
akan secara internal memanggilGetHashCode
dan untuk masing-masing poin dengan kode hash yang sama, akan memanggilEquals
untuk menentukan apakah sudah adaPoint
ketika Anda dapat membuat kelas yang mengimplementasikanIEqualityComparer<Point>
dan menjaga kompatibilitas dengan hal-hal lain yang bekerja denganPoint
sementara mendapatkan manfaat dari tidak memiliki yang miskinGetHashCode
dan kebutuhan untuk masukEquals()
.