Leistung von Arrays vs. Listen

191

Angenommen, Sie benötigen eine Liste / ein Array von Ganzzahlen, die Sie häufig iterieren müssen, und ich meine extrem oft. Die Gründe können variieren, aber sagen wir, es befindet sich im Herzen der innersten Schleife einer Verarbeitung mit hohem Volumen.

Im Allgemeinen würde man sich aufgrund ihrer flexiblen Größe für die Verwendung von Listen (Listen) entscheiden. Darüber hinaus behauptet die msdn-Dokumentation, dass Listen ein Array intern verwenden und genauso schnell arbeiten sollten (ein kurzer Blick mit Reflector bestätigt dies). Trotzdem ist ein gewisser Aufwand erforderlich.

Hat jemand das tatsächlich gemessen? Würde das 6-malige Durchlaufen einer Liste dieselbe Zeit in Anspruch nehmen wie ein Array?

Boas
quelle
3
Abgesehen von Leistungsproblemen bevorzuge ich die Verwendung von Arrays gegenüber Listen aufgrund ihrer festen Größe (in Fällen, in denen eine Änderung der Anzahl der Elemente natürlich nicht erforderlich ist). Wenn vorhandenen Code zu lesen, finde ich es hilfreich schnell zu wissen , dass ein Element gezwungen , eine feste Größe zu haben, anstatt den Code weiter unten in der Funktion zu prüfen.
Warren Stevens
2
T[]vs. List<T>kann einen großen Unterschied in der Leistung machen. Ich habe gerade eine extrem (verschachtelte) schleifenintensive Anwendung optimiert, um unter .NET 4.0 von Listen zu Arrays zu wechseln. Ich hatte eine Verbesserung von 5% bis 10% erwartet, aber eine Beschleunigung von über 40% erreicht! Keine anderen Änderungen als direkt von der Liste zum Array zu wechseln. Alle Aufzählungen wurden mit foreachAussagen durchgeführt. Basierend auf Marc Gravells Antwort sieht es so aus, als wäre es foreachmit List<T>besonders schlecht.
Spezielle Soße

Antworten:

221

Sehr einfach zu messen ...

In einer kleinen Anzahl von Verarbeitungscodes mit engen Schleifen, bei denen ich weiß, dass die Länge festgelegt ist, verwende ich Arrays für dieses zusätzliche kleine Stück Mikrooptimierung. Arrays können geringfügig schneller sein, wenn Sie den Indexer / für das Formular verwenden. IIRC ist jedoch der Ansicht, dass dies vom Datentyp im Array abhängt. Aber es sei denn , Sie brauchen , um Mikro-optimize, halten Sie es einfach und verwenden List<T>usw.

Dies gilt natürlich nur, wenn Sie alle Daten lesen. Ein Wörterbuch wäre für schlüsselbasierte Suchvorgänge schneller.

Hier sind meine Ergebnisse mit "int" (die zweite Zahl ist eine Prüfsumme, um zu überprüfen, ob alle die gleiche Arbeit geleistet haben):

(bearbeitet, um den Fehler zu beheben)

List/for: 1971ms (589725196)
Array/for: 1864ms (589725196)
List/foreach: 3054ms (589725196)
Array/foreach: 1860ms (589725196)

basierend auf dem Prüfstand:

using System;
using System.Collections.Generic;
using System.Diagnostics;
static class Program
{
    static void Main()
    {
        List<int> list = new List<int>(6000000);
        Random rand = new Random(12345);
        for (int i = 0; i < 6000000; i++)
        {
            list.Add(rand.Next(5000));
        }
        int[] arr = list.ToArray();

        int chk = 0;
        Stopwatch watch = Stopwatch.StartNew();
        for (int rpt = 0; rpt < 100; rpt++)
        {
            int len = list.Count;
            for (int i = 0; i < len; i++)
            {
                chk += list[i];
            }
        }
        watch.Stop();
        Console.WriteLine("List/for: {0}ms ({1})", watch.ElapsedMilliseconds, chk);

        chk = 0;
        watch = Stopwatch.StartNew();
        for (int rpt = 0; rpt < 100; rpt++)
        {
            for (int i = 0; i < arr.Length; i++)
            {
                chk += arr[i];
            }
        }
        watch.Stop();
        Console.WriteLine("Array/for: {0}ms ({1})", watch.ElapsedMilliseconds, chk);

        chk = 0;
        watch = Stopwatch.StartNew();
        for (int rpt = 0; rpt < 100; rpt++)
        {
            foreach (int i in list)
            {
                chk += i;
            }
        }
        watch.Stop();
        Console.WriteLine("List/foreach: {0}ms ({1})", watch.ElapsedMilliseconds, chk);

        chk = 0;
        watch = Stopwatch.StartNew();
        for (int rpt = 0; rpt < 100; rpt++)
        {
            foreach (int i in arr)
            {
                chk += i;
            }
        }
        watch.Stop();
        Console.WriteLine("Array/foreach: {0}ms ({1})", watch.ElapsedMilliseconds, chk);

        Console.ReadLine();
    }
}
Marc Gravell
quelle
8
Interessantes Detail: Hier sind die Zeiten RELEASE / DEBUG auf meinem Rig (.net 3.5 sp1): 0,92, 0,80, 0,96, 0,93; Dies sagt mir, dass die VM einige Informationen enthält, um die Array / for-Schleife ungefähr 10% besser zu optimieren als in den anderen Fällen.
David Schmitt
2
Ja, es gibt eine JIT-Optimierung für Array / für. Eigentlich hatte ich den Eindruck, dass es den Length-Fall enthielt (da es weiß, dass er behoben ist), weshalb ich ihn nicht zuerst herausgezogen habe (im Gegensatz zu List, wo ich ihn gemacht habe). Danke für das Update.
Marc Gravell
2
Seltsam. Meine sehr ähnlichen Tests zeigen keinen signifikanten Unterschied zwischen for und foreach bei der Verwendung von Arrays. Wird gründlich in einem Blog-Beitrag mit einem Benchmark untersuchen, den andere Leute ausführen können, und mir Ergebnisse für ...
Jon Skeet
1
Ich erhalte dramatisch unterschiedliche Ergebnisse, wenn ich für jeden Test eine andere Variable für chk verwende (chk1, chk2, chk3, chk4). Liste / für = 1303 ms, Array / für = 433 ms. Irgendwelche Ideen warum?
Jon
4
Der im obigen Kommentar von Jon erwähnte Link zu Jon Skeets Blog war defekt. Unten ist der aktualisierte Link. codeblog.jonskeet.uk/2009/01/29/…
Josh DeLong
88

Zusammenfassung:

  • Array muss verwendet werden:

    • So oft wie möglich. Es ist schnell und benötigt den kleinsten RAM-Bereich für die gleiche Menge an Informationen.
    • Wenn Sie die genaue Anzahl der benötigten Zellen kennen
    • Wenn Daten in Array <85000 b gespeichert sind (85000/32 = 2656 Elemente für ganzzahlige Daten)
    • Bei Bedarf hohe Direktzugriffsgeschwindigkeit
  • Liste muss verwendet werden:

    • Falls erforderlich, um Zellen am Ende der Liste hinzuzufügen (häufig)
    • Falls erforderlich, um Zellen am Anfang / in der Mitte der Liste hinzuzufügen (NICHT OFT)
    • Wenn Daten in Array <85000 b gespeichert sind (85000/32 = 2656 Elemente für ganzzahlige Daten)
    • Bei Bedarf hohe Direktzugriffsgeschwindigkeit
  • LinkedList muss Folgendes verwenden:

    • Bei Bedarf Zellen am Anfang / Mitte / Ende der Liste hinzufügen (häufig)
    • Bei Bedarf nur sequentieller Zugriff (vorwärts / rückwärts)
    • Wenn Sie GROSSE Elemente speichern müssen, die Anzahl der Elemente jedoch niedrig ist.
    • Verwenden Sie es besser nicht für eine große Anzahl von Elementen, da es zusätzlichen Speicher für Links verwendet.

Mehr Details:

Farbbedeutung

Array vs List vs Linked List

Viel mehr Details:

https://stackoverflow.com/a/29263914/4423545

Andrew
quelle
Ich bin etwas verwirrt über Ihre Behauptung, dass das Voranstellen von Listen relativ schnell ist, das Einfügen jedoch langsam. Das Einfügen ist ebenfalls eine lineare Zeit und im Durchschnitt um 50% schneller als das Einfügen.
Mike Marynowski
1
@MikeMarynowski in der c # -Liste ist ein Wrapper um Array. Wenn Sie also in eine Liste einfügen, haben Sie nur bis zu einem gewissen Punkt eine lineare Zeit. Nachdem dieses System ein neues größeres Array erstellt hat, benötigt es Zeit, um Elemente aus dem alten zu kopieren.
Andrew
Gleiches gilt für Präpenden.
Mike Marynowski
Eine Prepend-Operation ist nur eine Einfügung bei 0. Dies ist der Worst-Case-Insert in Bezug auf die Leistung. Wenn die Einfügung also langsam ist, ist die Voreinstellung noch langsamer.
Mike Marynowski
Sowohl Insert als auch Prepend ist O (n) (amortisiert). Ein Präfix ist eine Einfügung, aber es ist die langsamste Einfügung, da ALLE Elemente in der Liste um eine Stelle nach oben verschoben werden müssen. Eine Einfügung an einer zufälligen Stelle muss nur die Elemente nach oben verschieben, die einen höheren Index als die Einfügemarke haben, also durchschnittlich 50% der Elemente.
Mike Marynowski
26

Ich denke, die Leistung wird ziemlich ähnlich sein. Der Overhead, der bei der Verwendung einer Liste im Vergleich zu einem Array anfällt, ist IMHO, wenn Sie Elemente zur Liste hinzufügen und wenn die Liste die Größe des Arrays erhöhen muss, das intern verwendet wird, wenn die Kapazität des Arrays erreicht ist.

Angenommen, Sie haben eine Liste mit einer Kapazität von 10, dann erhöht die Liste ihre Kapazität, sobald Sie das 11. Element hinzufügen möchten. Sie können die Auswirkungen auf die Leistung verringern, indem Sie die Kapazität der Liste auf die Anzahl der darin enthaltenen Elemente initialisieren.

Aber um herauszufinden, ob das Durchlaufen einer Liste genauso schnell ist wie das Durchlaufen eines Arrays, warum vergleichen Sie es nicht?

int numberOfElements = 6000000;

List<int> theList = new List<int> (numberOfElements);
int[] theArray = new int[numberOfElements];

for( int i = 0; i < numberOfElements; i++ )
{
    theList.Add (i);
    theArray[i] = i;
}

Stopwatch chrono = new Stopwatch ();

chrono.Start ();

int j;

 for( int i = 0; i < numberOfElements; i++ )
 {
     j = theList[i];
 }

 chrono.Stop ();
 Console.WriteLine (String.Format("iterating the List took {0} msec", chrono.ElapsedMilliseconds));

 chrono.Reset();

 chrono.Start();

 for( int i = 0; i < numberOfElements; i++ )
 {
     j = theArray[i];
 }

 chrono.Stop ();
 Console.WriteLine (String.Format("iterating the array took {0} msec", chrono.ElapsedMilliseconds));

 Console.ReadLine();

Auf meinem System; Das Iterieren über das Array dauerte 33 ms. Das Durchlaufen der Liste dauerte 66 ms.

Um ehrlich zu sein, hatte ich nicht erwartet, dass die Variation so groß sein würde. Also habe ich meine Iteration in eine Schleife gesetzt: Jetzt führe ich beide Iterationen 1000 Mal aus. Die Ergebnisse sind:

Das Iterieren der Liste dauerte 67146 ms. Das Iterieren des Arrays dauerte 40821 ms

Jetzt ist die Variation nicht mehr so ​​groß, aber immer noch ...

Daher habe ich .NET Reflector gestartet und der Getter des Indexers der List-Klasse sieht folgendermaßen aus:

public T get_Item(int index)
{
    if (index >= this._size)
    {
        ThrowHelper.ThrowArgumentOutOfRangeException();
    }
    return this._items[index];
}

Wie Sie sehen können, führt die Liste bei Verwendung des Indexers der Liste eine Überprüfung durch, ob Sie die Grenzen des internen Arrays nicht überschreiten. Dieser zusätzliche Scheck ist kostenpflichtig.

Frederik Gheysels
quelle
Hallo Frederik, danke! Wie würden Sie erklären, dass Ihre Liste doppelt so lange gedauert hat wie die Arrays? Nicht das, was Sie erwarten würden. Haben Sie versucht, die Anzahl der Elemente zu erhöhen?
Boaz
1
Würde this._items [index] nicht zurückgeben; bereits eine Ausnahme auslösen, wenn der Index außerhalb des Bereichs lag? Warum hat .NET diese zusätzliche Prüfung, wenn das Endergebnis mit oder ohne gleich ist?
John Mercier
@ John Mercier Die Prüfung erfolgt anhand der Größe der Liste (Anzahl der derzeit enthaltenen Elemente), die sich von der Kapazität des _items-Arrays unterscheidet und wahrscheinlich darunter liegt. Das Array wird mit überschüssiger Kapazität zugewiesen, um das Hinzufügen zukünftiger Elemente zu beschleunigen, da nicht bei jeder Hinzufügung eine Neuzuweisung erforderlich ist.
Trasvi
21

Wenn Sie nur einen einzigen Wert aus einem der beiden Werte (nicht in einer Schleife) erhalten, überprüfen beide die Grenzen (Sie befinden sich im verwalteten Code, denken Sie daran). Die Liste führt dies nur zweimal aus. In den Anmerkungen später erfahren Sie, warum dies wahrscheinlich keine große Sache ist.

Wenn Sie Ihre eigene für verwenden (int int i = 0; i <x. [Länge / Anzahl]; i ++), ist der Hauptunterschied wie folgt:

  • Array:
    • Die Überprüfung der Grenzen wird entfernt
  • Listen
    • Die Überprüfung der Grenzen wird durchgeführt

Wenn Sie foreach verwenden, ist der Hauptunterschied wie folgt:

  • Array:
    • Für die Verwaltung der Iteration wird kein Objekt zugewiesen
    • Die Überprüfung der Grenzen wird entfernt
  • Liste über eine Variable, die als Liste bekannt ist.
    • Die Iterationsverwaltungsvariable wird dem Stapel zugewiesen
    • Die Überprüfung der Grenzen wird durchgeführt
  • Liste über eine Variable, die als IList bekannt ist.
    • Die Iterationsverwaltungsvariable wird dem Heap zugewiesen
    • Die Überprüfung der Grenzen wird ebenfalls durchgeführt. Listenwerte dürfen während des Foreach nicht geändert werden, wohingegen die Werte des Arrays geändert werden können.

Die Überprüfung der Grenzen ist oft keine große Sache (insbesondere, wenn Sie eine CPU mit einer tiefen Pipeline- und Verzweigungsvorhersage verwenden - die Norm für die meisten dieser Tage), aber nur Ihre eigene Profilerstellung kann Ihnen sagen, ob dies ein Problem ist. Wenn Sie sich in Teilen Ihres Codes befinden, in denen Sie Heap-Zuweisungen vermeiden (gute Beispiele sind Bibliotheken oder in Hashcode-Implementierungen), können Sie diese Gefahr vermeiden, indem Sie sicherstellen, dass die Variable als List not IList eingegeben wird. Wie immer Profil, wenn es darauf ankommt.

ShuggyCoUk
quelle
11

[ Siehe auch diese Frage ]

Ich habe Marc's Antwort so geändert, dass sie tatsächliche Zufallszahlen verwendet und in allen Fällen die gleiche Arbeit leistet.

Ergebnisse:

         für foreach
Array: 1575 ms 1575 ms (+ 0%)
Liste: 1630 ms 2627 ms (+ 61%)
         (+ 3%) (+ 67%)

(Prüfsumme: -1000038876)

Kompiliert als Release unter VS 2008 SP1. Wird ohne Debugging auf einem [email protected], .NET 3.5 SP1 ausgeführt.

Code:

class Program
{
    static void Main(string[] args)
    {
        List<int> list = new List<int>(6000000);
        Random rand = new Random(1);
        for (int i = 0; i < 6000000; i++)
        {
            list.Add(rand.Next());
        }
        int[] arr = list.ToArray();

        int chk = 0;
        Stopwatch watch = Stopwatch.StartNew();
        for (int rpt = 0; rpt < 100; rpt++)
        {
            int len = list.Count;
            for (int i = 0; i < len; i++)
            {
                chk += list[i];
            }
        }
        watch.Stop();
        Console.WriteLine("List/for: {0}ms ({1})", watch.ElapsedMilliseconds, chk);

        chk = 0;
        watch = Stopwatch.StartNew();
        for (int rpt = 0; rpt < 100; rpt++)
        {
            int len = arr.Length;
            for (int i = 0; i < len; i++)
            {
                chk += arr[i];
            }
        }
        watch.Stop();
        Console.WriteLine("Array/for: {0}ms ({1})", watch.ElapsedMilliseconds, chk);

        chk = 0;
        watch = Stopwatch.StartNew();
        for (int rpt = 0; rpt < 100; rpt++)
        {
            foreach (int i in list)
            {
                chk += i;
            }
        }
        watch.Stop();
        Console.WriteLine("List/foreach: {0}ms ({1})", watch.ElapsedMilliseconds, chk);

        chk = 0;
        watch = Stopwatch.StartNew();
        for (int rpt = 0; rpt < 100; rpt++)
        {
            foreach (int i in arr)
            {
                chk += i;
            }
        }
        watch.Stop();
        Console.WriteLine("Array/foreach: {0}ms ({1})", watch.ElapsedMilliseconds, chk);
        Console.WriteLine();

        Console.ReadLine();
    }
}
David Schmitt
quelle
Das ist komisch - ich habe gerade Ihren genauen Code ausgeführt, der über die Befehlszeile (3.5SP1) mit / o + / debug- erstellt wurde, und meine Ergebnisse sind: list / for: 1524; Array / für: 1472; Liste / foreach: 4128; Array / Foreach: 1484.
Jon Skeet
Sie sagen, dies wurde als Release kompiliert. Kann ich nur überprüfen, ob Sie es ausgeführt haben, anstatt es zu debuggen? Dumme Frage, ich weiß, aber ich kann die Ergebnisse nicht anders erklären ...
Jon Skeet
2

Die Messungen sind gut, aber Sie werden signifikant unterschiedliche Ergebnisse erhalten, je nachdem, was Sie genau in Ihrer inneren Schleife tun. Messen Sie Ihre eigene Situation. Wenn Sie Multithreading verwenden, ist dies allein keine triviale Aktivität.

Stephan Eggermont
quelle
2

Wenn Sie einige komplexe Berechnungen innerhalb der Schleife durchführen, ist die Leistung des Array-Indexers im Vergleich zum Listenindexer möglicherweise so gering, dass es letztendlich keine Rolle spielt.

Frederik Gheysels
quelle
2

Hier ist eines, das Wörterbücher verwendet, IEnumerable:

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;

static class Program
{
    static void Main()
    {
        List<int> list = new List<int>(6000000);

        for (int i = 0; i < 6000000; i++)
        {
                list.Add(i);
        }
        Console.WriteLine("Count: {0}", list.Count);

        int[] arr = list.ToArray();
        IEnumerable<int> Ienumerable = list.ToArray();
        Dictionary<int, bool> dict = list.ToDictionary(x => x, y => true);

        int chk = 0;
        Stopwatch watch = Stopwatch.StartNew();
        for (int rpt = 0; rpt < 100; rpt++)
        {
            int len = list.Count;
            for (int i = 0; i < len; i++)
            {
                chk += list[i];
            }
        }
        watch.Stop();
        Console.WriteLine("List/for: {0}ms ({1})", watch.ElapsedMilliseconds, chk);

        chk = 0;
        watch = Stopwatch.StartNew();
        for (int rpt = 0; rpt < 100; rpt++)
        {
            for (int i = 0; i < arr.Length; i++)
            {
                chk += arr[i];
            }
        }
        watch.Stop();
        Console.WriteLine("Array/for: {0}ms ({1})", watch.ElapsedMilliseconds, chk);

        chk = 0;
        watch = Stopwatch.StartNew();
        for (int rpt = 0; rpt < 100; rpt++)
        {
            foreach (int i in Ienumerable)
            {
                chk += i;
            }
        }

        Console.WriteLine("Ienumerable/for: {0}ms ({1})", watch.ElapsedMilliseconds, chk);

        chk = 0;
        watch = Stopwatch.StartNew();
        for (int rpt = 0; rpt < 100; rpt++)
        {
            foreach (int i in dict.Keys)
            {
                chk += i;
            }
        }

        Console.WriteLine("Dict/for: {0}ms ({1})", watch.ElapsedMilliseconds, chk);


        chk = 0;
        watch = Stopwatch.StartNew();
        for (int rpt = 0; rpt < 100; rpt++)
        {
            foreach (int i in list)
            {
                chk += i;
            }
        }

        watch.Stop();
        Console.WriteLine("List/foreach: {0}ms ({1})", watch.ElapsedMilliseconds, chk);

        chk = 0;
        watch = Stopwatch.StartNew();
        for (int rpt = 0; rpt < 100; rpt++)
        {
            foreach (int i in arr)
            {
                chk += i;
            }
        }
        watch.Stop();
        Console.WriteLine("Array/foreach: {0}ms ({1})", watch.ElapsedMilliseconds, chk);



        chk = 0;
        watch = Stopwatch.StartNew();
        for (int rpt = 0; rpt < 100; rpt++)
        {
            foreach (int i in Ienumerable)
            {
                chk += i;
            }
        }
        watch.Stop();
        Console.WriteLine("Ienumerable/foreach: {0}ms ({1})", watch.ElapsedMilliseconds, chk);

        chk = 0;
        watch = Stopwatch.StartNew();
        for (int rpt = 0; rpt < 100; rpt++)
        {
            foreach (int i in dict.Keys)
            {
                chk += i;
            }
        }
        watch.Stop();
        Console.WriteLine("Dict/foreach: {0}ms ({1})", watch.ElapsedMilliseconds, chk);

        Console.ReadLine();
    }
}
Travis
quelle
2

Versuchen Sie nicht, die Kapazität durch Erhöhen der Anzahl der Elemente zu erhöhen.

Performance

List For Add: 1ms
Array For Add: 2397ms

    Stopwatch watch;
        #region --> List For Add <--

        List<int> intList = new List<int>();
        watch = Stopwatch.StartNew();
        for (int rpt = 0; rpt < 60000; rpt++)
        {
            intList.Add(rand.Next());
        }
        watch.Stop();
        Console.WriteLine("List For Add: {0}ms", watch.ElapsedMilliseconds);
        #endregion

        #region --> Array For Add <--

        int[] intArray = new int[0];
        watch = Stopwatch.StartNew();
        int sira = 0;
        for (int rpt = 0; rpt < 60000; rpt++)
        {
            sira += 1;
            Array.Resize(ref intArray, intArray.Length + 1);
            intArray[rpt] = rand.Next();

        }
        watch.Stop();
        Console.WriteLine("Array For Add: {0}ms", watch.ElapsedMilliseconds);

        #endregion
Fatih GÜRDAL
quelle
Ich werde die Größe eines Arrays 60k mal langsam ändern ... In der Praxis besteht der Ansatz jedoch darin, zu überprüfen, wie viele zusätzliche Steckplätze Sie benötigen, die Größe auf + 60k zu ändern und dann durch die Einfügungen zu zippen.
tobriand
Das Ändern der Größe eines Arrays ist sehr schnell, wenn Sie die Größe jedes Mal verdoppeln, wenn Sie feststellen, dass Sie mehr Speicherplatz benötigen. Ich habe festgestellt, dass dies ungefähr genauso lange dauert wie die einmalige Größenänderung nach der ersten Deklaration. Das gibt Ihnen die Flexibilität einer Liste und den größten Teil der Geschwindigkeit eines Arrays.
user1318499
2

Ich befürchtete, dass die in anderen Antworten veröffentlichten Benchmarks dem Compiler noch Raum lassen würden, Schleifen zu optimieren, zu eliminieren oder zusammenzuführen, und schrieb eine, die:

  • Verwendete unvorhersehbare Eingaben (zufällig)
  • Führt eine Berechnung mit dem auf der Konsole gedruckten Ergebnis aus
  • Ändert die Eingabedaten bei jeder Wiederholung

Das Ergebnis ist, dass ein direktes Array eine um 250% bessere Leistung aufweist als ein Zugriff auf ein in eine IList eingeschlossenes Array:

  • 1 Milliarde Array-Zugriffe: 4000 ms
  • 1 Milliarde Listenzugriffe: 10000 ms
  • 100 Millionen Array-Zugriffe: 350 ms
  • 100 Millionen Listenzugriffe: 1000 ms

Hier ist der Code:

static void Main(string[] args) {
  const int TestPointCount = 1000000;
  const int RepetitionCount = 1000;

  Stopwatch arrayTimer = new Stopwatch();
  Stopwatch listTimer = new Stopwatch();

  Point2[] points = new Point2[TestPointCount];
  var random = new Random();
  for (int index = 0; index < TestPointCount; ++index) {
    points[index].X = random.NextDouble();
    points[index].Y = random.NextDouble();
  }

  for (int repetition = 0; repetition <= RepetitionCount; ++repetition) {
    if (repetition > 0) { // first repetition is for cache warmup
      arrayTimer.Start();
    }
    doWorkOnArray(points);
    if (repetition > 0) { // first repetition is for cache warmup
      arrayTimer.Stop();
    }

    if (repetition > 0) { // first repetition is for cache warmup
      listTimer.Start();
    }
    doWorkOnList(points);
    if (repetition > 0) { // first repetition is for cache warmup
      listTimer.Stop();
    }
  }

  Console.WriteLine("Ignore this: " + points[0].X + points[0].Y);
  Console.WriteLine(
    string.Format(
      "{0} accesses on array took {1} ms",
      RepetitionCount * TestPointCount, arrayTimer.ElapsedMilliseconds
    )
  );
  Console.WriteLine(
    string.Format(
      "{0} accesses on list took {1} ms",
      RepetitionCount * TestPointCount, listTimer.ElapsedMilliseconds
    )
  );

}

private static void doWorkOnArray(Point2[] points) {
  var random = new Random();

  int pointCount = points.Length;

  Point2 accumulated = Point2.Zero;
  for (int index = 0; index < pointCount; ++index) {
    accumulated.X += points[index].X;
    accumulated.Y += points[index].Y;
  }

  accumulated /= pointCount;

  // make use of the result somewhere so the optimizer can't eliminate the loop
  // also modify the input collection so the optimizer can merge the repetition loop
  points[random.Next(0, pointCount)] = accumulated;
}

private static void doWorkOnList(IList<Point2> points) {
  var random = new Random();

  int pointCount = points.Count;

  Point2 accumulated = Point2.Zero;
  for (int index = 0; index < pointCount; ++index) {
    accumulated.X += points[index].X;
    accumulated.Y += points[index].Y;
  }

  accumulated /= pointCount;

  // make use of the result somewhere so the optimizer can't eliminate the loop
  // also modify the input collection so the optimizer can merge the repetition loop
  points[random.Next(0, pointCount)] = accumulated;
}
Cygon
quelle
0

Da List <> Arrays intern verwendet, sollte die Grundleistung dieselbe sein. Zwei Gründe, warum die Liste möglicherweise etwas langsamer ist:

  • Um ein Element in der Liste nachzuschlagen, wird eine List-Methode aufgerufen, die die Suche im zugrunde liegenden Array durchführt. Sie benötigen dort also einen zusätzlichen Methodenaufruf. Andererseits könnte der Compiler dies erkennen und den "unnötigen" Abruf optimieren.
  • Der Compiler kann einige spezielle Optimierungen vornehmen, wenn er die Größe des Arrays kennt, die er für eine Liste unbekannter Länge nicht durchführen kann. Dies kann zu einer Leistungsverbesserung führen, wenn Ihre Liste nur wenige Elemente enthält.

Um zu überprüfen, ob es für Sie einen Unterschied macht, passen Sie die angegebenen Timing-Funktionen wahrscheinlich am besten an eine Liste der Größe an, die Sie verwenden möchten, und sehen Sie, wie die Ergebnisse für Ihren Sonderfall aussehen.

etw
quelle
0

Da ich eine ähnliche Frage hatte, bekam ich einen schnellen Start.

Meine Frage ist etwas spezifischer: Was ist die schnellste Methode für eine reflexive Array-Implementierung?

Die von Marc Gravell durchgeführten Tests zeigen viel, aber nicht genau das Timing des Zugriffs. Sein Timing beinhaltet auch das Durchlaufen der Arrays und Listen. Da ich auch eine dritte Methode gefunden habe, die ich testen wollte, ein 'Wörterbuch', um sie zu vergleichen, habe ich den Hist-Testcode erweitert.

Firts, ich mache einen Test mit einer Konstanten, die mir ein bestimmtes Timing einschließlich der Schleife gibt. Dies ist ein "nackter" Zeitpunkt, der den tatsächlichen Zugriff ausschließt. Dann mache ich einen Test mit Zugriff auf die Subjektstruktur, dies gibt mir und Overhead inklusive Timing, Looping und tatsächlichen Zugriff.

Der Unterschied zwischen "nacktem" Timing und "Overhead-eingeschlossenem" Timing gibt mir einen Hinweis auf das "Strukturzugriff" -Zeitpunkt.

Aber wie genau ist dieses Timing? Während des Tests werden die Fenster einige Zeit in Scheiben geschnitten, um die Sicherheit zu gewährleisten. Ich habe keine Informationen über das Time Slicing, aber ich gehe davon aus, dass es während des Tests gleichmäßig verteilt ist und in der Größenordnung von zehn ms liegt, was bedeutet, dass die Genauigkeit für das Timing in der Größenordnung von +/- 100 ms oder so liegen sollte. Eine etwas grobe Schätzung? Auf jeden Fall eine Quelle für einen systematischen Mearure-Fehler.

Außerdem wurden die Tests im Debug-Modus ohne Optimierung durchgeführt. Andernfalls kann der Compiler den tatsächlichen Testcode ändern.

Ich erhalte also zwei Ergebnisse, eines für eine Konstante mit der Bezeichnung '(c)' und eines für den mit '(n)' gekennzeichneten Zugriff. Die Differenz 'dt' gibt an, wie viel Zeit der tatsächliche Zugriff benötigt.

Und das sind die Ergebnisse:

          Dictionary(c)/for: 1205ms (600000000)
          Dictionary(n)/for: 8046ms (589725196)
 dt = 6841

                List(c)/for: 1186ms (1189725196)
                List(n)/for: 2475ms (1779450392)
 dt = 1289

               Array(c)/for: 1019ms (600000000)
               Array(n)/for: 1266ms (589725196)
 dt = 247

 Dictionary[key](c)/foreach: 2738ms (600000000)
 Dictionary[key](n)/foreach: 10017ms (589725196)
 dt = 7279

            List(c)/foreach: 2480ms (600000000)
            List(n)/foreach: 2658ms (589725196)
 dt = 178

           Array(c)/foreach: 1300ms (600000000)
           Array(n)/foreach: 1592ms (589725196)
 dt = 292


 dt +/-.1 sec   for    foreach
 Dictionary     6.8       7.3
 List           1.3       0.2
 Array          0.2       0.3

 Same test, different system:
 dt +/- .1 sec  for    foreach
 Dictionary     14.4   12.0
       List      1.7    0.1
      Array      0.5    0.7

Mit besseren Schätzungen der Zeitfehler (wie kann der systematische Messfehler aufgrund von Zeitscheiben beseitigt werden?) Könnte mehr über die Ergebnisse gesagt werden.

Es sieht so aus, als hätte List / foreach den schnellsten Zugriff, aber der Overhead bringt ihn um.

Der Unterschied zwischen List / for und List / foreach ist seltsam. Vielleicht ist etwas Geld erforderlich?

Für den Zugriff auf ein Array spielt es keine Rolle, ob Sie eine forSchleife oder eine foreachSchleife verwenden. Die Timing-Ergebnisse und ihre Genauigkeit machen die Ergebnisse "vergleichbar".

Die Verwendung eines Wörterbuchs ist bei weitem am langsamsten. Ich habe es nur in Betracht gezogen, weil ich auf der linken Seite (dem Indexer) eine spärliche Liste von Ganzzahlen habe und keinen Bereich, wie er in diesen Tests verwendet wird.

Hier ist der geänderte Testcode.

Dictionary<int, int> dict = new Dictionary<int, int>(6000000);
List<int> list = new List<int>(6000000);
Random rand = new Random(12345);
for (int i = 0; i < 6000000; i++)
{
    int n = rand.Next(5000);
    dict.Add(i, n);
    list.Add(n);
}
int[] arr = list.ToArray();

int chk = 0;
Stopwatch watch = Stopwatch.StartNew();
for (int rpt = 0; rpt < 100; rpt++)
{
    int len = dict.Count;
    for (int i = 0; i < len; i++)
    {
        chk += 1; // dict[i];
    }
}
watch.Stop();
long c_dt = watch.ElapsedMilliseconds;
Console.WriteLine("         Dictionary(c)/for: {0}ms ({1})", c_dt, chk);

chk = 0;
watch = Stopwatch.StartNew();
for (int rpt = 0; rpt < 100; rpt++)
{
    int len = dict.Count;
    for (int i = 0; i < len; i++)
    {
        chk += dict[i];
    }
}
watch.Stop();
long n_dt = watch.ElapsedMilliseconds;
Console.WriteLine("         Dictionary(n)/for: {0}ms ({1})", n_dt, chk);
Console.WriteLine("dt = {0}", n_dt - c_dt);

watch = Stopwatch.StartNew();
for (int rpt = 0; rpt < 100; rpt++)
{
    int len = list.Count;
    for (int i = 0; i < len; i++)
    {
        chk += 1; // list[i];
    }
}
watch.Stop();
c_dt = watch.ElapsedMilliseconds;
Console.WriteLine("               List(c)/for: {0}ms ({1})", c_dt, chk);

watch = Stopwatch.StartNew();
for (int rpt = 0; rpt < 100; rpt++)
{
    int len = list.Count;
    for (int i = 0; i < len; i++)
    {
        chk += list[i];
    }
}
watch.Stop();
n_dt = watch.ElapsedMilliseconds;
Console.WriteLine("               List(n)/for: {0}ms ({1})", n_dt, chk);
Console.WriteLine("dt = {0}", n_dt - c_dt);

chk = 0;
watch = Stopwatch.StartNew();
for (int rpt = 0; rpt < 100; rpt++)
{
    for (int i = 0; i < arr.Length; i++)
    {
        chk += 1; // arr[i];
    }
}
watch.Stop();
c_dt = watch.ElapsedMilliseconds;
Console.WriteLine("              Array(c)/for: {0}ms ({1})", c_dt, chk);

chk = 0;
watch = Stopwatch.StartNew();
for (int rpt = 0; rpt < 100; rpt++)
{
    for (int i = 0; i < arr.Length; i++)
    {
        chk += arr[i];
    }
}
watch.Stop();
n_dt = watch.ElapsedMilliseconds;
Console.WriteLine("Array(n)/for: {0}ms ({1})", n_dt, chk);
Console.WriteLine("dt = {0}", n_dt - c_dt);

chk = 0;
watch = Stopwatch.StartNew();
for (int rpt = 0; rpt < 100; rpt++)
{
    foreach (int i in dict.Keys)
    {
        chk += 1; // dict[i]; ;
    }
}
watch.Stop();
c_dt = watch.ElapsedMilliseconds;
Console.WriteLine("Dictionary[key](c)/foreach: {0}ms ({1})", c_dt, chk);

chk = 0;
watch = Stopwatch.StartNew();
for (int rpt = 0; rpt < 100; rpt++)
{
    foreach (int i in dict.Keys)
    {
        chk += dict[i]; ;
    }
}
watch.Stop();
n_dt = watch.ElapsedMilliseconds;
Console.WriteLine("Dictionary[key](n)/foreach: {0}ms ({1})", n_dt, chk);
Console.WriteLine("dt = {0}", n_dt - c_dt);

chk = 0;
watch = Stopwatch.StartNew();
for (int rpt = 0; rpt < 100; rpt++)
{
    foreach (int i in list)
    {
        chk += 1; // i;
    }
}
watch.Stop();
c_dt = watch.ElapsedMilliseconds;
Console.WriteLine("           List(c)/foreach: {0}ms ({1})", c_dt, chk);

chk = 0;
watch = Stopwatch.StartNew();
for (int rpt = 0; rpt < 100; rpt++)
{
    foreach (int i in list)
    {
        chk += i;
    }
}
watch.Stop();
n_dt = watch.ElapsedMilliseconds;
Console.WriteLine("           List(n)/foreach: {0}ms ({1})", n_dt, chk);
Console.WriteLine("dt = {0}", n_dt - c_dt);

chk = 0;
watch = Stopwatch.StartNew();
for (int rpt = 0; rpt < 100; rpt++)
{
    foreach (int i in arr)
    {
        chk += 1; // i;
    }
}
watch.Stop();
c_dt = watch.ElapsedMilliseconds;
Console.WriteLine("          Array(c)/foreach: {0}ms ({1})", c_dt, chk);

chk = 0;
watch = Stopwatch.StartNew();
for (int rpt = 0; rpt < 100; rpt++)
{
    foreach (int i in arr)
    {
        chk += i;
    }
}
watch.Stop();
n_dt = watch.ElapsedMilliseconds;
Console.WriteLine("Array(n)/foreach: {0}ms ({1})", n_dt, chk);
Console.WriteLine("dt = {0}", n_dt - c_dt);
PapaAtHome
quelle
0

In einigen kurzen Tests habe ich festgestellt, dass eine Kombination aus beiden besser ist, was ich als einigermaßen intensive Mathematik bezeichnen würde:

Art: List<double[]>

Zeit: 00: 00: 05.1861300

Art: List<List<double>>

Zeit: 00: 00: 05.7941351

Art: double[rows * columns]

Zeit: 00: 00: 06.0547118

Ausführen des Codes:

int rows = 10000;
int columns = 10000;

IMatrix Matrix = new IMatrix(rows, columns);

Stopwatch stopwatch = new Stopwatch();
stopwatch.Start();


for (int r = 0; r < Matrix.Rows; r++)
    for (int c = 0; c < Matrix.Columns; c++)
        Matrix[r, c] = Math.E;

for (int r = 0; r < Matrix.Rows; r++)
    for (int c = 0; c < Matrix.Columns; c++)
        Matrix[r, c] *= -Math.Log(Math.E);


stopwatch.Stop();
TimeSpan ts = stopwatch.Elapsed;

Console.WriteLine(ts.ToString());

Ich wünschte, wir hätten einige erstklassige Hardware Accelerated Matrix-Klassen, wie sie das .NET-Team mit dem gemacht hat System.Numerics.Vectors Klasse gemacht hat!

C # könnte die beste ML-Sprache mit etwas mehr Arbeit in diesem Bereich sein!

rostiger Nagel
quelle