102 Stimmen

List<T>.Contains() ist sehr langsam?

Kann mir jemand erklären, warum die Generika List.Contains() Funktion so langsam ist?

Ich habe eine List<long> mit etwa einer Million Zahlen und dem Code, der ständig prüft, ob es eine bestimmte Zahl innerhalb dieser Zahlen gibt.

Ich habe versucht, das Gleiche zu tun mit Dictionary<long, byte> und die Dictionary.ContainsKey() Funktion und war etwa 10-20 Mal schneller als mit der Liste.

Natürlich möchte ich das Wörterbuch nicht wirklich für diesen Zweck verwenden, denn dafür ist es nicht gedacht.

Die eigentliche Frage ist also, ob es eine Alternative zur List<T>.Contains() aber nicht so abgefahren wie Dictionary<K,V>.ContainsKey() ?

173voto

Marc Gravell Punkte 970173

Wenn Sie nur prüfen, ob es sie gibt, HashSet<T> in .NET 3.5 ist die beste Option - wörterbuchähnliche Leistung, aber kein Schlüssel/Wert-Paar - nur die Werte:

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

33voto

Frederik Gheysels Punkte 54908

List.Contains ist eine O(n)-Operation.

Dictionary.ContainsKey ist eine O(1)-Operation, da sie den Hashcode der Objekte als Schlüssel verwendet, was eine schnellere Suchmöglichkeit bietet.

Ich glaube nicht, dass es eine gute Idee ist, eine Liste mit einer Million Einträgen zu durchsuchen, um ein paar Einträge zu finden.

Ist es nicht möglich, diese Millionen von Entitäten z.B. in einem RDBMS zu speichern und Abfragen in dieser Datenbank durchzuführen?

Wenn das nicht möglich ist, würde ich trotzdem ein Dictionary verwenden, wenn Sie Schlüssel nachschlagen wollen.

9voto

Kevin North Punkte 121

Ich glaube, ich habe die Antwort! Ja, es stimmt, dass Contains() auf einer Liste (Array) O(n) ist, aber wenn das Array kurz ist und Sie Werttypen verwenden, sollte es trotzdem recht schnell sein. Aber mit dem CLR Profiler [kostenloser Download von Microsoft], entdeckte ich, dass Contains() ist Boxing-Werte, um sie zu vergleichen, die Heap-Zuweisung, die SEHR teuer (langsam) erfordert. [Hinweis: Dies ist .Net 2.0; andere .Net-Versionen wurden nicht getestet.]

Hier ist die ganze Geschichte und die Lösung. Wir haben eine Aufzählung namens "VI" und eine Klasse namens "ValueIdList", die ein abstrakter Typ für eine Liste (Array) von VI-Objekten ist. Die ursprüngliche Implementierung stammte aus den alten .Net 1.1 Tagen und verwendete eine gekapselte ArrayList. Wir entdeckten kürzlich in http://blogs.msdn.com/b/joshwil/archive/2004/04/13/112598.aspx dass eine generische Liste (List<VI>) bei Wertetypen (wie unserem enum VI) viel besser funktioniert als ArrayList, weil die Werte nicht gepackt werden müssen. Das stimmt und es hat funktioniert... fast.

Der CLR Profiler enthüllte eine Überraschung. Hier ist ein Teil des Allocation Graph:

  • ValueIdList::Contains bool(VI) 5.5MB (34.81%)
  • Generic.List::Contains bool(<UNKNOWN>) 5.5MB (34.81%)
  • Generic.ObjectEqualityComparer<T>::Equals bool (<UNKNOWN> <UNKNOWN>) 5.5MB (34.88%)
  • Werte.VI 7.7MB (49.03%)

Wie Sie sehen können, ruft Contains() überraschenderweise Generic.ObjectEqualityComparer.Equals() auf, was offensichtlich das Boxing eines VI-Wertes erfordert, was eine teure Heap-Zuweisung erfordert. Es ist merkwürdig, dass Microsoft das Boxing in der Liste abschafft, um es dann für eine einfache Operation wie diese wieder zu verlangen.

Unsere Lösung bestand darin, die Contains()-Implementierung neu zu schreiben, was in unserem Fall leicht zu bewerkstelligen war, da wir das generische Listenobjekt (_items) bereits gekapselt hatten. Hier ist der einfache Code:

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

Der Vergleich von VI-Werten wird jetzt in unserer eigenen Version von IndexOf() durchgeführt, die kein Boxing erfordert und sehr schnell ist. Unser spezielles Programm wurde nach dieser einfachen Neuprogrammierung um 20 % beschleunigt. O(n)... kein Problem! Vermeiden Sie einfach die Speichervergeudung!

5voto

Stefan Steinegger Punkte 62197

Wörterbuch ist gar nicht so schlecht, denn die Schlüssel in einem Wörterbuch sind darauf ausgelegt, schnell gefunden zu werden. Um eine Zahl in einer Liste zu finden, muss die gesamte Liste durchlaufen werden.

Das Wörterbuch funktioniert natürlich nur, wenn die Zahlen eindeutig und nicht geordnet sind.

Ich denke, es gibt auch eine HashSet<T> Klasse in .NET 3.5, erlaubt sie auch nur eindeutige Elemente.

4voto

user2023861 Punkte 7454

Dies ist nicht gerade eine Antwort auf Ihre Frage, aber ich habe eine Klasse, die die Leistung von Contains() auf eine Sammlung erhöht. Ich habe eine Queue unterklassifiziert und ein Dictionary hinzugefügt, das Hashcodes auf Listen von Objekten abbildet. Die Dictionary.Contains() Funktion ist O(1), während List.Contains() , Queue.Contains() y Stack.Contains() sind O(n).

Der Wertetyp des Wörterbuchs ist eine Warteschlange, die Objekte mit demselben Hashcode enthält. Der Aufrufer kann ein benutzerdefiniertes Klassenobjekt liefern, das IEqualityComparer implementiert. Sie könnten dieses Muster für Stacks oder Lists verwenden. Der Code müsste nur geringfügig geändert werden.

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

Meine einfachen Tests zeigen, dass meine HashQueue.Contains() läuft viel schneller als Queue.Contains() . Die Ausführung des Testcodes mit einer Zählung von 10.000 dauert 0,00045 Sekunden für die HashQueue-Version und 0,37 Sekunden für die Queue-Version. Bei einer Anzahl von 100.000 benötigt die HashQueue-Version 0,0031 Sekunden, während die Queue-Version 36,38 Sekunden benötigt!

Hier ist mein Testcode:

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

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X