HashSet <Point> neden HashSet <string> 'den daha yavaş?


165

Bazı piksel konumlarını kopyalara izin vermeden saklamak istedim, bu yüzden akla ilk gelen şey HashSet<Point>benzer sınıflar. Ancak bu gibi bir şeye kıyasla çok yavaş görünüyor HashSet<string>.

Örneğin, bu kod:

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

yaklaşık 22.5 saniye sürer.

Aşağıdaki kod (bariz nedenlerle iyi bir seçim değildir) sadece 1.6 saniye sürer:

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

Yani, sorularım:

  • Bunun bir sebebi var mı? Bu cevabı kontrol ettim , ama 22.5 saniye o cevapta gösterilen rakamlardan çok daha fazla.
  • Yinelenmeden puan depolamanın daha iyi bir yolu var mı?


Birleştirilmiş dizeleri kullanmamak için bu "açık nedenler" nelerdir? Kendi IEqualityComparer'ımı uygulamak istemiyorsam bunu yapmanın daha iyi yolu nedir?
Ivan Yurchenko

Yanıtlar:


290

Nokta yapısından kaynaklanan iki mükemmel problem vardır. Console.WriteLine(GC.CollectionCount(0));Test koduna eklediğinizde görebileceğiniz bir şey . Puan testinin ~ 3720 koleksiyon gerektirdiğini, ancak string testinin yalnızca ~ 18 koleksiyon gerektirdiğini göreceksiniz. Bedava değil. Bir değer türü gördüğünüzde çok fazla koleksiyon ortaya çıkarsa, o zaman "uh-oh, çok fazla boks" yapmalısınız.

Sorun, işini yapmak için HashSet<T>bir ihtiyacı var IEqualityComparer<T>. Bir tane sağlamadığınız için, tarafından iade edilene geri düşmesi gerekir EqualityComparer.Default<T>(). Bu yöntem dize için iyi bir iş yapabilir, IEquatable uygular. Ancak Point için değil, .NET 1.0'dan yararlanan ve jenerikleri asla sevmeyen bir tür. Yapabileceği tek şey Object yöntemlerini kullanmak.

Diğer bir sorun ise Point.GetHashCode () 'un bu testte çok fazla çarpışma yapmaması, bu yüzden çok fazla çarpışma olması, bu nedenle Object.Equals ()' i oldukça ağır bir şekilde kırmasıdır. String mükemmel bir GetHashCode uygulamasına sahiptir.

HashSet'e iyi bir karşılaştırıcı sağlayarak her iki sorunu da çözebilirsiniz. Bunun gibi:

class PointComparer : IEqualityComparer<Point> {
    public bool Equals(Point x, Point y) {
        return x.X == y.X && x.Y == y.Y;
    }

    public int GetHashCode(Point obj) {
        // Perfect hash for practical bitmaps, their width/height is never >= 65536
        return (obj.Y << 16) ^ obj.X;
    }
}

Ve kullanın:

HashSet<Point> list = new HashSet<Point>(new PointComparer());

Ve şimdi yaklaşık 150 kat daha hızlı, dize testini kolayca atıyor.


26
GetHashCode yöntemi uygulamasını sağlamak için +1. Sadece merak için, belirli bir obj.X << 16 | obj.Y;uygulama ile nasıl geldiniz ?
Akash KC

32
Farenin pencerelerdeki konumunu geçirme şeklinden ilham aldı. Görüntülemek istediğiniz herhangi bir bitmap için mükemmel bir karmadır.
Hans Passant

2
Bunu bilmek iyi. Sizinki gibi karma kod yazmak için herhangi bir belge veya en iyi kılavuz? Aslında, yukarıdaki karma kodun deneyiminizle veya takip ettiğiniz herhangi bir kılavuzla birlikte gelip gelmediğini hala bilmek istiyorum.
Akash KC

5
@AkashKC C # ile pek tecrübem yok ama bildiğim kadarıyla tamsayılar genellikle 32 bit. Bu durumda, 2 sayının karmasını istiyorsunuz ve bir 16 bit'i sola kaydırarak, her sayının "alt" 16 bitinin diğerini "etkilemediğinden" emin olursunuz |. 3 sayı için 22 ve 11'i vardiya olarak kullanmak mantıklı olabilir. 4 sayı için 24, 16, 8 olur. Ancak yine de çarpışmalar olur, ancak sadece sayılar büyürse. Ama aynı zamanda çok önemli bir şekilde HashSetuygulamaya bağlıdır . Eğer "bit kesme" ile açık adresleme kullanırsa (sanmıyorum!) Sola kaydırma yaklaşım kötü olabilir.
MSeifert

3
@HansPassant: GetHashCode'da OR yerine XOR kullanmanın biraz daha iyi olup olmadığını merak ediyorum - nokta koordinatlarının 16 biti aşması durumunda (belki de ortak ekranlarda değil, yakın gelecekte). // XOR genellikle hash işlevlerinde OR'den daha iyidir, çünkü daha az bilgi kaybeder, tersine çevrilir, vb. // örn. Negatif koordinatlara izin verilirse, Y negatifse X katkısına ne olacağını düşünün.
Krazy Glew

85

Performans düşüşünün ana nedeni tüm boksların devam etmesidir (Hans Passant'ın cevabında daha önce açıklandığı gibi ).

Bunun dışında, karma kod algoritması sorunu daha da kötüleştirir, çünkü Equals(object obj)boks dönüşümlerinin miktarını arttırmak için daha fazla çağrıya neden olur .

Ayrıca , karma kodununPoint tarafından hesaplandığını unutmayın x ^ y. Bu, veri aralığınızda çok az dağılım üretir ve bu nedenle bunların kovaları HashSetaşırı doldurulur - gerçekleşmeyen string, karmaların dağılımının çok daha büyük olduğu bir şey.

Bu sorunu kendi Pointyapınızı (önemsiz) uygulayarak ve beklenen veri aralığınız için daha iyi bir karma algoritma kullanarak, örneğin koordinatları kaydırarak çözebilirsiniz :

(x << 16) ^ y

Karma kodlar konusunda bazı iyi tavsiyeler için Eric Lippert'in konuyla ilgili blog gönderisini okuyun .


4
Point referans kaynağına bakarak GetHashCodegerçekleştirir: unchecked(x ^ y)süre için stringçok daha karmaşık görünüyor ..
Gilad Green

2
Hmm .. peki, varsayım doğruysa, ben sadece kullanarak çalıştı kontrol etmek HashSet<long>()yerine, ve kullanılan list.Add(unchecked(x ^ y));HashSet değerleri ekleyin. Bu aslında HashSet<string> (345 ms) ' den bile daha hızlıydı . Bu, tarif ettiğinizden bir şekilde farklı mı?
Ahmed Abdelhameed

4
@AhmedAbdelhameed, muhtemelen karma kümenize fark ettiğinizden daha az üye eklediğiniz için (yine karma kod algoritmasının korkunç dağılımı nedeniyle). listDoldurmayı bitirdiğinizde sayı nedir ?
Inbetween

4
@AhmedAbdelhameed Testiniz yanlış. Aynı uzunlukları tekrar tekrar ekliyorsunuz, bu yüzden eklediğiniz yalnızca birkaç öğe var. Yerleştirirken point, HashSetdahili olarak arayacak GetHashCodeve aynı Equals
hash koduna

49
Uygulamak için gerek yok Pointbir sınıf oluşturmak zaman bunu uygulayan IEqualityComparer<Point>ve diğer şeylerle tutmak uyumluluğu ile çalışmak olduğunu Pointfakir olmaması faydasını alırken GetHashCodeve kutunun gereğini de Equals().
Jon Hanna
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.