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()
?
Jawaban:
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
sumber
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.
sumber
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:
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!
sumber
Contains
Penerapan kustom jauh lebih cepat untuk kasus penggunaan saya.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.sumber
Sebuah SortedList akan lebih cepat untuk mencari (tapi lebih lambat untuk menyisipkan item)
sumber
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) sedangkanList.Contains()
,Queue.Contains()
, danStack.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 daripadaQueue.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(); }
sumber
HashQueue, 00:00:00.0004029
Queue, 00:00:00.3901439
HashSet, 00:00:00.0001716
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.
sumber
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.
sumber
string
referensi danbool
nilai 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 antarastring
danbool
mungkin dengan demikian tidak membuat perbedaan dalam ukuran sama sekali. String kosong""
selalu ada di memori sebagai properti statisstring.Empty
, jadi tidak ada bedanya apakah Anda menggunakannya di kamus atau tidak. (Dan itu digunakan di tempat lain.)