Daftar <T> .Contains () sangat lambat?

94

Adakah yang bisa menjelaskan mengapa List.Contains()fungsi generik sangat lambat?

Saya memiliki List<long>sekitar satu juta nomor, dan kode yang terus-menerus memeriksa apakah ada nomor tertentu di dalam nomor ini.

Saya mencoba melakukan hal yang sama dengan menggunakan Dictionary<long, byte>dan Dictionary.ContainsKey()fungsinya, dan itu sekitar 10-20 kali lebih cepat daripada dengan List.

Tentu saja, saya tidak ingin menggunakan Dictionary untuk tujuan itu, karena itu tidak dimaksudkan untuk digunakan seperti itu.

Jadi, pertanyaan sebenarnya di sini adalah, apakah ada alternatif lain List<T>.Contains(), tapi tidak sekeren itu Dictionary<K,V>.ContainsKey()?

DSent
sumber
2
Apa masalah dengan Kamus? Ini dimaksudkan untuk digunakan dalam kasus seperti milik Anda.
Kamarey
4
@Kamarey: Hash mungkin merupakan pilihan yang lebih baik.
Brian Rasmussen
HashSet adalah apa yang saya cari.
DSent

Jawaban:

160

Jika Anda hanya memeriksa keberadaan, HashSet<T>di .NET 3.5 adalah opsi terbaik Anda - kinerja seperti kamus, tetapi tidak ada pasangan kunci / nilai - hanya nilainya:

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

List.Contains adalah operasi O (n).

Dictionary.ContainsKey adalah operasi O (1), karena menggunakan kode hash objek sebagai kunci, yang memberi Anda kemampuan pencarian lebih cepat.

Menurut saya, tidak ada baiknya memiliki List yang berisi jutaan entri. Saya tidak berpikir bahwa kelas List dirancang untuk tujuan itu. :)

Bukankah mungkin untuk menyimpan entitas millon tersebut ke dalam RDBMS misalnya, dan melakukan kueri pada database tersebut?

Jika tidak memungkinkan, saya akan tetap menggunakan Dictionary.

Frederik Gheysels
sumber
13
Menurut saya tidak ada yang tidak pantas tentang daftar dengan sejuta item, hanya saja Anda mungkin tidak ingin terus menjalankan pencarian linier di dalamnya.
Akankah Dean
Setuju, tidak ada yang salah dengan list atau array dengan banyak entri. Jangan memindai nilai.
Michael Krauklis
8

Saya rasa saya punya jawabannya! Ya, benar bahwa Contains () pada list (array) adalah O (n), tetapi jika arraynya pendek dan Anda menggunakan tipe nilai, itu tetap harus cukup cepat. Tetapi dengan menggunakan CLR Profiler [unduh gratis dari Microsoft], saya menemukan bahwa Contains () adalah nilai-nilai tinju untuk membandingkannya, yang membutuhkan alokasi tumpukan, yang SANGAT mahal (lambat). [Catatan: Ini adalah .Net 2.0; Versi .Net lainnya tidak diuji.]

Berikut cerita lengkap dan solusinya. Kami memiliki enumerasi yang disebut "VI" dan membuat kelas yang disebut "ValueIdList" yang merupakan tipe abstrak untuk daftar (array) objek VI. Implementasi asli dalam .Net kuno 1,1 hari, dan itu menggunakan ArrayList yang dienkapsulasi. Kami baru-baru ini menemukan di http://blogs.msdn.com/b/joshwil/archive/2004/04/13/112598.aspx bahwa daftar generik (List <VI>) berkinerja jauh lebih baik daripada ArrayList pada tipe nilai (seperti enum VI) karena nilainya tidak harus dikotakkan. Itu benar dan berhasil ... hampir.

CLR Profiler mengungkapkan kejutan. Berikut sebagian dari Grafik Alokasi:

  • ValueIdList :: Berisi bool (VI) 5.5MB (34.81%)
  • Generic.List :: Berisi bool (<UNKNOWN>) 5.5MB (34.81%)
  • Generic.ObjectEqualityComparer <T> :: Sama dengan bool (<UNKNOWN> <UNKNOWN>) 5.5MB (34.88%)
  • Values.VI 7.7MB (49.03%)

Seperti yang Anda lihat, Contains () secara mengejutkan memanggil Generic.ObjectEqualityComparer.Equals (), yang tampaknya memerlukan pengemasan nilai VI, yang memerlukan alokasi heap yang mahal. Aneh bahwa Microsoft akan menghapus tinju dalam daftar, hanya memerlukannya lagi untuk operasi sederhana seperti ini.

Solusi kami adalah menulis ulang implementasi Contains (), yang dalam kasus kami mudah dilakukan karena kami sudah merangkum objek daftar generik (_items). Berikut kode sederhananya:

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

Perbandingan nilai VI sekarang sedang dilakukan dalam versi IndexOf () kita sendiri yang tidak memerlukan tinju, dan ini sangat cepat. Program khusus kami dipercepat hingga 20% setelah penulisan ulang sederhana ini. O (n) ... tidak masalah! Hindari saja penggunaan memori yang terbuang percuma!

Kevin North
sumber
Terima kasih atas tipnya, saya sendiri terjebak oleh kinerja tinju yang buruk. ContainsPenerapan kustom jauh lebih cepat untuk kasus penggunaan saya.
Lea Hayes
5

Kamus tidak seburuk itu, karena tombol dalam kamus dirancang untuk ditemukan dengan cepat. Untuk menemukan nomor dalam daftar itu perlu mengulang melalui seluruh daftar.

Tentu saja kamus hanya berfungsi jika nomor Anda unik dan tidak berurutan.

Saya rasa ada juga HashSet<T>kelas di .NET 3.5, itu juga hanya memungkinkan elemen unik.

Stefan Steinegger
sumber
Dictionary <Type, integer> juga dapat menyimpan objek non-unik secara efektif - gunakan integer untuk menghitung jumlah duplikat. Misalnya, Anda akan menyimpan daftar {a, b, a} sebagai {a = 2, b = 1}. Itu memang kehilangan penahbisan, tentu saja.
MSalters
2

Sebuah SortedList akan lebih cepat untuk mencari (tapi lebih lambat untuk menyisipkan item)

Mitch Wheat
sumber
2

Ini bukanlah jawaban yang tepat untuk pertanyaan Anda, tetapi saya memiliki kelas yang meningkatkan kinerja Contains () pada sebuah koleksi. Saya membuat subclass Queue dan menambahkan Dictionary yang memetakan kode hash ke daftar objek. The Dictionary.Contains()fungsi O (1) sedangkan List.Contains(), Queue.Contains(), dan Stack.Contains()adalah O (n).

Tipe nilai kamus adalah antrian yang menampung objek dengan kode hash yang sama. Pemanggil dapat menyediakan objek kelas khusus yang mengimplementasikan IEqualityComparer. Anda dapat menggunakan pola ini untuk Tumpukan atau Daftar. Kode hanya perlu beberapa perubahan.

/// <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
    }
}

Pengujian sederhana saya menunjukkan bahwa HashQueue.Contains()lari saya jauh lebih cepat daripada Queue.Contains(). Menjalankan kode tes dengan hitungan diatur ke 10.000 membutuhkan 0,00045 detik untuk versi HashQueue dan 0,37 detik untuk versi Antrian. Dengan hitungan 100.000, versi HashQueue membutuhkan 0,0031 detik sedangkan Antrian membutuhkan 36,38 detik!

Inilah kode pengujian saya:

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();
}
pengguna2023861
sumber
Saya baru saja menambahkan kasus uji ke-3 untuk HashSet <T> yang tampaknya mendapatkan hasil yang lebih baik daripada solusi Anda: HashQueue, 00:00:00.0004029 Queue, 00:00:00.3901439 HashSet, 00:00:00.0001716
psulek
1

Mengapa kamus tidak pantas?

Untuk melihat apakah nilai tertentu ada dalam daftar, Anda perlu menelusuri seluruh daftar. Dengan kamus (atau wadah berbasis hash lainnya), jauh lebih cepat mempersempit jumlah objek yang perlu Anda bandingkan. Kuncinya (dalam kasus Anda, angka) di-hash dan yang memberikan kamus subset pecahan dari objek untuk dibandingkan.

Andrew
sumber
0

Saya menggunakan ini dalam Kerangka Kerja Kompak di mana tidak ada dukungan untuk HashSet, saya telah memilih Kamus di mana kedua string adalah nilai yang saya cari.

Itu berarti saya mendapatkan fungsionalitas daftar <> dengan kinerja kamus. Agak hacky, tapi berhasil.

Mark McGookin
sumber
1
Jika Anda menggunakan Dictionary sebagai pengganti HashSet, Anda sebaiknya menyetel nilainya menjadi "" daripada string yang sama dengan kuncinya. Dengan begitu Anda akan menggunakan lebih sedikit memori. Atau Anda bahkan bisa menggunakan Dictionary <string, bool> dan mengatur semuanya ke true (atau false). Saya tidak tahu mana yang akan menggunakan lebih sedikit memori, string kosong atau bool. Tebakan saya pasti konyol.
TTT
Dalam kamus, stringreferensi dan boolnilai membuat perbedaan 3 atau 7 byte, masing-masing untuk sistem 32 atau 64 bit. Namun, perhatikan bahwa ukuran setiap entri dibulatkan menjadi kelipatan 4 atau 8, masing-masing. Pilihan antara stringdan boolmungkin dengan demikian tidak membuat perbedaan dalam ukuran sama sekali. String kosong ""selalu ada di memori sebagai properti statis string.Empty, jadi tidak ada bedanya apakah Anda menggunakannya di kamus atau tidak. (Dan itu digunakan di tempat lain.)
Wormbo