web-dev-qa-db-ger.com

Lookups optimieren: Wörterbuchschlüssel-Lookups vs. Array-Index-Lookups

Ich schreibe einen 7-Karten-Poker-Hand-Evaluator als eines meiner Pet-Projekte. Bei dem Versuch, die Geschwindigkeit zu optimieren (ich mag die Herausforderung), war ich schockiert, als ich herausfand, dass die Leistung der Wörterbuchschlüssel-Lookups im Vergleich zu Array-Index-Suchergebnissen recht langsam war.

Zum Beispiel habe ich diesen Beispielcode ausgeführt, der über alle 52 Aufzählungen hinausläuft. 7 = 133.784.560 mögliche 7 Kartenhände auswählen:

var intDict = new Dictionary<int, int>();
var intList = new List<int>();
for (int i = 0; i < 100000; i ++)
{
    intDict.Add(i, i);  
    intList.Add(i);
}

int result;

var sw = new Stopwatch();
sw.Start();
for (int card1 = 0; card1 < 46; card1++)
  for (int card2 = card1 + 1; card2 < 47; card2++)
    for (int card3 = card2 + 1; card3 < 48; card3++)
      for (int card4 = card3 + 1; card4 < 49; card4++)
        for (int card5 = card4 + 1; card5 < 50; card5++)
          for (int card6 = card5 + 1; card6 < 51; card6++)
            for (int card7 = card6 + 1; card7 < 52; card7++)
              result = intDict[32131]; // perform C(52,7) dictionary key lookups
sw.Stop();
Console.WriteLine("time for dictionary lookups: {0} ms", sw.ElapsedMilliseconds);

sw.Reset();

sw.Start();
for (int card1 = 0; card1 < 46; card1++)
  for (int card2 = card1 + 1; card2 < 47; card2++)
    for (int card3 = card2 + 1; card3 < 48; card3++)
      for (int card4 = card3 + 1; card4 < 49; card4++)
        for (int card5 = card4 + 1; card5 < 50; card5++)
          for (int card6 = card5 + 1; card6 < 51; card6++)
            for (int card7 = card6 + 1; card7 < 52; card7++)
              result = intList[32131]; // perform C(52,7) array index lookups
sw.Stop();
Console.WriteLine("time for array index lookups: {0} ms", sw.ElapsedMilliseconds);

welche Ausgänge:

time for dictionary lookups: 2532 ms
time for array index lookups: 313 ms

Wird ein solches Verhalten erwartet (Leistungsabfall um den Faktor 8)? IIRC, ein Dictionary hat im Durchschnitt O(1) Lookups, während ein Array O(1)-Lookups im schlimmsten Fall hat, also erwarte ich, dass die Array-Lookups schneller sind aber nicht so viel!

Im Moment speichere ich Poker-Hand-Ranglisten in einem Wörterbuch. Ich denke, wenn dies so schnell ist, wie das Wörterbuch nachschlagen kann, muss ich meine Herangehensweise überdenken und Arrays verwenden. Die Indexierung der Rankings wird jedoch etwas kompliziert und ich werde wahrscheinlich eine weitere Frage dazu stellen müssen.

28
snazzer

Vergessen Sie nicht, dass Big-O-Notationen nur angeben, wie die Komplexität in Bezug auf die Größe (usw.) zunimmt - es gibt keinen Hinweis auf die konstanten Faktoren. Deshalb ist manchmal sogar ein linearer search für Schlüssel schneller als eine Wörterbuchsuche, wenn ausreichend wenige Schlüssel vorhanden sind. In diesem Fall führen Sie jedoch nicht einmal eine Suche mit dem Array durch - es handelt sich lediglich um eine direkte Indizierung.

Für direkte Index-Lookups sind Arrays im Grunde ideal - es handelt sich lediglich um 

pointer_into_array = base_pointer + offset * size

(Und dann einen Zeiger dereference.)

Die Suche nach einem Wörterbuch ist relativ kompliziert - im Vergleich zu einer linearen Suche nach Schlüsseln sehr schnell, wenn viele Schlüssel vorhanden sind, aber viel komplizierter als eine direkte Array-Suche. Es muss den Hash des Schlüssels berechnen, herausfinden, in welchem ​​Bucket sich der Hash befinden soll, möglicherweise doppelte Hashes (oder doppelte Buckets) behandeln und dann auf Gleichheit prüfen.

Wählen Sie wie immer die richtige Datenstruktur für den Job - und wenn Sie wirklich nur mit der Indizierung in ein Array (oder List<T>) davonkommen können, dann ja, das ist unglaublich schnell.

57
Jon Skeet

Wird ein solches Verhalten erwartet (Leistungsabfall um den Faktor 8)?

Warum nicht? Jede Array-Suche ist fast unbemerkt/vernachlässigbar, wohingegen eine Wörterbuchsuche mindestens einen zusätzlichen Unterprogrammaufruf erfordert.

Wenn beide Elemente O(1) sind, bedeutet dies, dass selbst wenn Sie in jeder Sammlung 50-mal mehr Elemente haben, der Leistungsabfall immer nur ein Faktor dessen ist (8).

8
ChrisW

Etwas könnte ein Jahrtausend dauern und immer noch O (1) sein.

Wenn Sie diesen Code im Disassembly-Fenster einzeln durchlaufen, werden Sie schnell den Unterschied erkennen.

5
Mike Dunlavey

Wörterbuchstrukturen sind am nützlichsten, wenn der Schlüsselraum sehr groß ist und nicht in einer stabilen, sequenzierten Reihenfolge abgebildet werden kann. Wenn Sie Ihre Schlüssel in eine relativ kleine Zahl in einem relativ kleinen Bereich konvertieren können, wird es Ihnen schwer fallen, eine Datenstruktur zu finden, die besser ist als ein Array.

Auf einem Implementierungshinweis; In .NET sind Wörterbücher im Wesentlichen Hashwörter. Sie können die Leistung der Schlüsselsuche verbessern, indem Sie sicherstellen, dass Ihre Schlüssel einen großen Bereich eindeutiger Werte bilden. Es sieht so aus, als würden Sie in Ihrem Fall eine einfache Ganzzahl als Schlüssel verwenden (was meiner Meinung nach auf seinen eigenen Wert wirkt) - so dass dies möglicherweise das Beste ist, das Sie tun können.

3
LBushkin

Bei einer Array-Suche geht es um das Schnellste, was Sie tun können - im Wesentlichen ist es nur ein Zeiger-Arithmetik-Element, das vom Anfang des Arrays zu dem Element führt, das Sie suchen wollten. Auf der anderen Seite ist das Nachschlagen des Wörterbuchs wahrscheinlich etwas langsamer, da es Hashing durchführen muss und sich darum bemüht, den richtigen Eimer zu finden. Obwohl die erwartete Laufzeit auch O(1) ist, sind die algorithmischen Konstanten größer, sodass sie langsamer sind.

2

Willkommen bei der Big-O-Notation. Man muss immer berücksichtigen, dass es einen konstanten Faktor gibt.

Ein Dict-Lookup ist natürlich viel teurer als ein Array-Lookup.

Big-O sagt Ihnen nur, wie Algorithmen skalieren. Verdoppeln Sie die Anzahl der Suchvorgänge und sehen Sie, wie sich die Zahlen ändern: Beide sollten ungefähr doppelt so lange dauern.

2
ebo

Die Kosten für das Abrufen eines Elements aus einem Dictionary betragen O(1) . Dies liegt jedoch daran, dass ein Dictionary als Hashtabelle implementiert ist. Daher müssen Sie zuerst den Hashwert berechnen, um zu wissen, welches Element verwendet werden soll Rückkehr. Hashtables sind oft nicht so effizient - aber sie eignen sich für große Datensätze oder Datensätze mit vielen eindeutigen Hashwerten.

Die Liste (abgesehen davon, dass es sich um ein Müllwort handelt, mit dem ein Array anstatt einer verknüpften Liste beschrieben wird!), Ist schneller, da es den Wert zurückgibt, indem das zurückgegebene Element direkt berechnet wird.

0
gbjbaanb