В C#, Список.Содержит() - слишком медленно?
кто-нибудь может объяснить мне, почему список дженериков содержит() функция так медленно?
У меня есть список с примерно миллионом номеров и код, который постоянно проверяет, есть ли определенное число в этих числах.
Я попытался сделать то же самое с помощью словаря и функции ContainsKey (), и это было примерно в 10-20 раз быстрее, чем со списком.
Конечно, я действительно не хочу использовать словарь для этой цели, потому что он не должен был использоваться путь.
Итак, реальный вопрос здесь заключается в том, есть ли альтернатива списку.Содержит (), но не так странно, как словарь.ContainsKey() ?
Заранее спасибо!
8 ответов:
Если вы просто проверяете существование,
HashSet<T>
в .NET 3.5 это ваш лучший вариант-словарь, как производительность, но нет пары ключ / значение-только значения:HashSet<int> data = new HashSet<int>(); for (int i = 0; i < 1000000; i++) { data.Add(rand.Next(50000000)); } bool contains = data.Contains(1234567); // etc
список.Содержит операцию O(n).
словарь.ContainsKey-это операция O(1), поскольку она использует хэш-код объектов в качестве ключа, что дает вам более быструю возможность поиска.
Я не думаю, что это хорошая идея, чтобы иметь список, который содержит более миллиона записей. Я не думаю, что класс List был разработан для этой цели. :)
разве нельзя сохранить эти объекты millon в СУБД, например, и выполнить запросы на это база данных ?
Если это невозможно, то я бы использовал словарь в любом случае.
Я думаю, что у меня есть ответ! Да, это правда, что Contains () в списке (массиве) - O(n), но если массив короткий и вы используете типы значений, он все равно должен быть довольно быстрым. Но с помощью профилировщика CLR [скачать бесплатно от Microsoft] я обнаружил, что Contains () - это боксерские значения для их сравнения, что требует выделения кучи, что очень дорого (медленно). [Примечание: это .Net 2.0; другие версии .Net не тестировались.]
вот и решение. У нас есть перечисление называется " VI "и сделал класс под названием" ValueIdList", который является абстрактным типом для списка (массива) объектов VI. Первоначальная реализация была в древних .Net 1.1 дней, и он использовал инкапсулированный ArrayList. Мы обнаружили недавно в http://blogs.msdn.com/b/joshwil/archive/2004/04/13/112598.aspx что общий список (List
) работает намного лучше, чем ArrayList на типах значений (например, наше перечисление VI), потому что значения не должны быть упакованы. Это правда и это работал... почти. профилировщик CLR показал сюрприз. Вот часть графика распределения:
- ValueIdList:: содержит bool (VI) 5.5 MB (34.81%)
- универсальный.Список:: содержит bool () 5.5 MB (34.81%)
- универсальный.ObjectEqualityComparer
:: Equals bool ( ) 5.5 MB (34.88%) - Values.VI 7,7 МБ (49,03%)
Как вы можете видеть, содержит () удивительно вызовы Родовой.ObjectEqualityComparer.Equals (), что, по-видимому, требует упаковки значения VI, что требует дорогостоящего выделения кучи. Странно, что Microsoft исключила бы бокс в списке, только чтобы снова потребовать его для простой операции, такой как эта.
наше решение состояло в том, чтобы переписать реализацию Contains (), что в нашем случае было легко сделать, так как мы уже инкапсулировали общий объект списка (_items). Вот простой код:
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 теперь выполняется в нашей собственной версии IndexOf (), которая не требует бокса, и это очень быстро. Наша конкретная программа ускорилась на 20% после этого простого перезаписи. O (n)... без проблем! Просто избегайте впустую использования памяти!
словарь не так уж плох, потому что ключи в словаре предназначены для быстрого поиска. Чтобы найти число в списке, ему нужно перебрать весь список.
конечно, словарь работает только в том случае, если ваши номера уникальны и не упорядочены.
Я думаю, что есть также
HashSet<T>
класс в .NET 3.5, он также позволяет только уникальные элементы.
A SortedList будет быстрее искать (но медленнее вставлять элементы)
это не совсем ответ на ваш вопрос, но у меня есть класс, который увеличивает производительность содержит() на коллекции. Я подкласс очереди и добавил словарь, который отображает хэш-коды в списки объектов. Элемент
Dictionary.Contains()
функция O (1) тогда какList.Contains()
,Queue.Contains()
иStack.Contains()
являются O (n).тип значения словаря-это очередь, содержащая объекты с тем же хэш-кодом. Вызывающий объект может предоставить пользовательский объект класса, реализующий IEqualityComparer. Вы могли бы используйте этот шаблон для стеков или списков. Код потребует всего лишь нескольких изменений.
/// <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 } }
мое простое тестирование показывает, что мой
HashQueue.Contains()
работает гораздо быстрее, чемQueue.Contains()
. Выполнение тестового кода со значением счетчика 10 000 занимает 0,00045 секунды для версии HashQueue и 0,37 секунды для версии очереди. При количестве 100 000 версия HashQueue занимает 0,0031 секунды, тогда как очередь занимает 36,38 секунды!вот мой тестовый код:
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(); }
Почему словарь не подходит?
чтобы увидеть, если определенное значение находится в списке, вам нужно пройти весь список. С помощью словаря (или другого контейнера на основе хэша) гораздо быстрее сузить количество объектов, с которыми вам нужно сравнить. Ключ (в вашем случае, число) хэшируется, и это дает словарю дробное подмножество объектов для сравнения.