Liste <T> .Contains () çok mu yavaş?


94

Biri bana jenerik List.Contains()işlevinin neden bu kadar yavaş olduğunu açıklayabilir mi?

Bir var List<long>yaklaşık bir milyon sayılar ve bu sayıların içinde belirli sayıda varsa sürekli kontrol ediyor koduyla.

Aynı şeyi Dictionary<long, byte>ve Dictionary.ContainsKey()işlevini kullanarak yapmayı denedim ve Listeden yaklaşık 10-20 kat daha hızlıydı.

Elbette, Sözlüğü bu amaçla kullanmak istemiyorum, çünkü bu şekilde kullanılması amaçlanmadı.

Öyleyse, buradaki asıl soru şudur, bunun alternatifi var mı List<T>.Contains(), ama o kadar çılgın değil Dictionary<K,V>.ContainsKey()mi?


2
Sözlük ile ilgili sorun nedir? Sizinki gibi durumlarda kullanılmak üzere tasarlanmıştır.
Kamarey

4
@Kamarey: HashSet daha iyi bir seçenek olabilir.
Brian Rasmussen

HashSet aradığım şeydi.
DSent

Yanıtlar:


160

Yalnızca var olup olmadığını kontrol ediyorsanız, HashSet<T>.NET 3.5'te en iyi seçeneğiniz - sözlük benzeri performans, ancak anahtar / değer çifti yok - yalnızca değerler:

    HashSet<int> data = new HashSet<int>();
    for (int i = 0; i < 1000000; i++)
    {
        data.Add(rand.Next(50000000));
    }
    bool contains = data.Contains(1234567); // etc

30

List.Contains bir O (n) işlemidir.

Dictionary.ContainsKey bir O (1) işlemidir, çünkü nesnelerin karma kodunu anahtar olarak kullanır, bu da size daha hızlı arama yeteneği sağlar.

Milyonlarca giriş içeren bir Listeye sahip olmanın iyi bir fikir olduğunu sanmıyorum. List sınıfının bu amaç için tasarlandığını sanmıyorum. :)

Bu milyon varlıkları örneğin bir RDBMS'ye kaydetmek ve bu veritabanı üzerinde sorgulamalar yapmak mümkün değil mi?

Mümkün değilse, yine de bir Sözlük kullanırdım.


13
Milyon öğeli bir liste hakkında uygunsuz bir şey olduğunu sanmıyorum, sadece muhtemelen üzerinde doğrusal aramalar yapmaya devam etmek istemiyorsunuzdur.
Will Dean

Kabul edildi, ne bir listede ne de bu kadar çok giriş içeren bir dizide yanlış bir şey yok. Değerler için tarama yapmayın.
Michael Krauklis

8

Sanırım cevabım var! Evet, bir listede (dizi) İçerir () 'in O (n) olduğu doğrudur, ancak dizi kısaysa ve değer türlerini kullanıyorsanız, yine de oldukça hızlı olmalıdır. Ancak CLR Profiler'ı [Microsoft'tan ücretsiz indirerek] kullanarak, Contains () 'in değerleri karşılaştırmak için kutulama değerleri olduğunu keşfettim, bu da ÇOK pahalı (yavaş) yığın ayırma gerektirir. [Not: Bu .Net 2.0'dır; diğer .Net sürümleri test edilmemiştir.]

İşte tam hikaye ve çözüm. "VI" adında bir numaralandırmamız var ve VI nesnelerinin bir listesi (dizisi) için soyut bir tür olan "ValueIdList" adlı bir sınıf oluşturduk. Orijinal uygulama eski .Net 1.1 günlerindeydi ve kapsüllenmiş bir ArrayList kullanıyordu. Yakın zamanda http://blogs.msdn.com/b/joshwil/archive/2004/04/13/112598.aspx adresinde keşfettik genel bir listenin (List <VI>) değer türleri üzerinde ArrayList'ten çok daha iyi performans gösterdiğini (bizim gibi enum VI) çünkü değerlerin kutu içine alınması gerekmez. Bu doğru ve işe yaradı ... neredeyse.

CLR Profiler bir sürpriz ortaya çıkardı. Tahsis Grafiğinin bir bölümü aşağıda verilmiştir:

  • ValueIdList :: bool (VI) 5.5MB (% 34.81) içerir
  • Generic.List :: bool (<UNKNOWN>) 5.5MB (% 34.81) içerir
  • Generic.ObjectEqualityComparer <T> :: Eşittir bool (<UNKNOWN> <UNKNOWN>) 5.5MB (% 34.88)
  • Değerler.VI 7,7MB (% 49,03)

Gördüğünüz gibi, Contains () şaşırtıcı bir şekilde Generic.ObjectEqualityComparer.Equals () 'ı çağırır, bu da görünüşe göre pahalı yığın tahsisi gerektiren bir VI değerinin kutulanmasını gerektirir. Microsoft'un listedeki kutuyu ortadan kaldırması, sadece bunun gibi basit bir işlem için tekrar gerektirmesi garip.

Bizim çözümümüz Contains () uygulamasını yeniden yazmaktı, ki bu bizim durumumuzda zaten genel liste nesnesini (_items) kapsüllediğimiz için yapmak kolaydı. İşte basit kod:

public bool Contains(VI id) 
{
  return IndexOf(id) >= 0;
}

public int IndexOf(VI id) 
{ 
  int i, count;

  count = _items.Count;
  for (i = 0; i < count; i++)
    if (_items[i] == id)
      return i;
  return -1;
}

public bool Remove(VI id) 
{
  int i;

  i = IndexOf(id);
  if (i < 0)
    return false;
  _items.RemoveAt(i);

  return true;
}

VI değerlerinin karşılaştırması artık kendi IndexOf () sürümümüzde yapılıyor ki bu kutulama gerektirmez ve çok hızlıdır. Bu basit yeniden yazma işleminden sonra özel programımız% 20 hızlandı. O (n) ... sorun değil! Boşa harcanan bellek kullanımından kaçının!


Bahşiş için teşekkürler, ben de kötü boks performansına yakalanmıştım. Özel bir Containsuygulama, kullanım durumum için çok daha hızlı.
Lea Hayes

5

Sözlük o kadar da kötü değil çünkü sözlükteki anahtarlar hızlı bulunacak şekilde tasarlandı. Listede bir numara bulmak için tüm listeyi yinelemesi gerekir.

Elbette sözlük yalnızca sayılarınız benzersizse ve sıralanmamışsa çalışır.

Sanırım HashSet<T>.NET 3.5'te de bir sınıf var, ayrıca sadece benzersiz elemanlara izin veriyor.


Bir Sözlük <Tür, tamsayı> benzersiz olmayan nesneleri de etkili bir şekilde depolayabilir - yinelenenlerin sayısını saymak için tamsayıyı kullanın. Örneğin, {a, b, a} listesini {a = 2, b = 1} olarak kaydedersiniz. Elbette buyruğunu kaybediyor.
MSalters


2

Bu tam olarak sorunuzun cevabı değil, ancak Contains () 'in bir koleksiyondaki performansını artıran bir sınıfım var. Bir Kuyruğun alt sınıfını oluşturdum ve hashcode'ları nesne listelerine eşleyen bir Sözlük ekledim. Dictionary.Contains()İşlev O (1), oysa olduğu List.Contains(), Queue.Contains()ve Stack.Contains()O (n) 'dir.

Sözlüğün değer türü, aynı hashcode ile nesneleri tutan bir kuyruktur. Çağıran, IEqualityComparer uygulayan özel bir sınıf nesnesi sağlayabilir. Bu kalıbı Yığınlar veya Listeler için kullanabilirsiniz. Kodun yalnızca birkaç değişikliğe ihtiyacı olacaktır.

/// <summary>
/// This is a class that mimics a queue, except the Contains() operation is O(1) rather     than O(n) thanks to an internal dictionary.
/// The dictionary remembers the hashcodes of the items that have been enqueued and dequeued.
/// Hashcode collisions are stored in a queue to maintain FIFO order.
/// </summary>
/// <typeparam name="T"></typeparam>
private class HashQueue<T> : Queue<T>
{
    private readonly IEqualityComparer<T> _comp;
    public readonly Dictionary<int, Queue<T>> _hashes; //_hashes.Count doesn't always equal base.Count (due to collisions)

    public HashQueue(IEqualityComparer<T> comp = null) : base()
    {
        this._comp = comp;
        this._hashes = new Dictionary<int, Queue<T>>();
    }

    public HashQueue(int capacity, IEqualityComparer<T> comp = null) : base(capacity)
    {
        this._comp = comp;
        this._hashes = new Dictionary<int, Queue<T>>(capacity);
    }

    public HashQueue(IEnumerable<T> collection, IEqualityComparer<T> comp = null) :     base(collection)
    {
        this._comp = comp;

        this._hashes = new Dictionary<int, Queue<T>>(base.Count);
        foreach (var item in collection)
        {
            this.EnqueueDictionary(item);
        }
    }

    public new void Enqueue(T item)
    {
        base.Enqueue(item); //add to queue
        this.EnqueueDictionary(item);
    }

    private void EnqueueDictionary(T item)
    {
        int hash = this._comp == null ? item.GetHashCode() :     this._comp.GetHashCode(item);
        Queue<T> temp;
        if (!this._hashes.TryGetValue(hash, out temp))
        {
            temp = new Queue<T>();
            this._hashes.Add(hash, temp);
        }
        temp.Enqueue(item);
    }

    public new T Dequeue()
    {
        T result = base.Dequeue(); //remove from queue

        int hash = this._comp == null ? result.GetHashCode() : this._comp.GetHashCode(result);
        Queue<T> temp;
        if (this._hashes.TryGetValue(hash, out temp))
        {
            temp.Dequeue();
            if (temp.Count == 0)
                this._hashes.Remove(hash);
        }

        return result;
    }

    public new bool Contains(T item)
    { //This is O(1), whereas Queue.Contains is (n)
        int hash = this._comp == null ? item.GetHashCode() : this._comp.GetHashCode(item);
        return this._hashes.ContainsKey(hash);
    }

    public new void Clear()
    {
        foreach (var item in this._hashes.Values)
            item.Clear(); //clear collision lists

        this._hashes.Clear(); //clear dictionary

        base.Clear(); //clear queue
    }
}

Basit testlerim, HashQueue.Contains()daha hızlı çalıştığımı gösteriyor Queue.Contains(). Test kodunu sayım 10.000 olarak ayarlanmış olarak çalıştırmak, HashQueue sürümü için 0.00045 saniye ve Kuyruk sürümü için 0.37 saniye sürer. 100.000 sayımla HashQueue sürümü 0.0031 saniye, Kuyruk ise 36.38 saniye sürüyor!

İşte test kodum:

static void Main(string[] args)
{
    int count = 10000;

    { //HashQueue
        var q = new HashQueue<int>(count);

        for (int i = 0; i < count; i++) //load queue (not timed)
            q.Enqueue(i);

        System.Diagnostics.Stopwatch sw = System.Diagnostics.Stopwatch.StartNew();
        for (int i = 0; i < count; i++)
        {
            bool contains = q.Contains(i);
        }
        sw.Stop();
        Console.WriteLine(string.Format("HashQueue, {0}", sw.Elapsed));
    }

    { //Queue
        var q = new Queue<int>(count);

        for (int i = 0; i < count; i++) //load queue (not timed)
            q.Enqueue(i);

        System.Diagnostics.Stopwatch sw = System.Diagnostics.Stopwatch.StartNew();
        for (int i = 0; i < count; i++)
        {
            bool contains = q.Contains(i);
        }
        sw.Stop();
        Console.WriteLine(string.Format("Queue,     {0}", sw.Elapsed));
    }

    Console.ReadLine();
}

HashSet <T> için sizin çözümünüzden daha iyi sonuçlar alacak gibi görünen 3. test durumunu ekledim: HashQueue, 00:00:00.0004029 Queue, 00:00:00.3901439 HashSet, 00:00:00.0001716
psulek

1

Sözlük neden uygunsuz?

Listede belirli bir değer olup olmadığını görmek için tüm listeyi dolaşmanız gerekir. Bir sözlükle (veya başka bir karma tabanlı kapla) karşılaştırmanız gereken nesnelerin sayısını daraltmak çok daha hızlıdır. Anahtar (sizin durumunuzda, sayı) karma hale getirilmiştir ve bu, sözlüğe karşılaştırılacak nesnelerin kesirli alt kümesini verir.


0

Bunu, HashSet desteği olmayan Kompakt Çerçevede kullanıyorum, her iki dizenin de aradığım değer olduğu bir Sözlük seçtim.

Sözlük performansı ile liste <> işlevselliği elde ettiğim anlamına geliyor. Biraz huysuz ama işe yarıyor.


1
Bir HashSet yerine bir Sözlük kullanıyorsanız, değeri anahtarla aynı dizeden ziyade "" olarak da ayarlayabilirsiniz. Bu şekilde daha az hafıza kullanırsınız. Alternatif olarak Dictionary <string, bool> kullanabilir ve hepsini true (veya false) olarak ayarlayabilirsiniz. Hangisinin daha az bellek, boş bir dizge veya bool kullanacağını bilmiyorum. Tahminim bool olurdu.
TTT

Sözlükte, bir stringreferans ve bir booldeğer, sırasıyla 32 veya 64 bit sistemler için 3 veya 7 baytlık bir fark yaratır. Bununla birlikte, her bir girişin boyutunun sırasıyla 4 veya 8'in katlarına yuvarlandığını unutmayın. Arasındaki seçim stringve boolkudreti dolayısıyla hiç boyutunda fark etmez. Boş dize ""her zaman bellekte zaten statik özellik olarak mevcuttur string.Empty, bu nedenle sözlükte kullanıp kullanmamanız fark etmez. (Ve yine de başka yerlerde kullanılıyor.)
Wormbo
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.