Ursprünglich wollte ich wissen, ob ToList
mehr Speicher reserviert als der Konstruktor von List<T>
, der ein IEnumerable<T>
(kein Unterschied) benötigt.
Zu Testzwecken habe ich Enumerable.Range
verwendet, um ein Quell-Array zu erstellen, mit dem ich eine Instanz von List<int>
über 1. ToList
und 2. Konstruktor . Beide erstellen Kopien.
Auf diese Weise habe ich einen großen Unterschied beim Speicherverbrauch festgestellt:
Enumerable.Range(1, 10000000)
oder Enumerable.Range(1, 10000000).ToArray()
Wenn ich den ersten Aufruf verwende und ToList
aufruft, dann erzeugt das resultierende Objekt ~ 60% mehr Speicher als das Array (38,26MB / 64MB).
F: Was ist der Grund dafür oder wo ist mein Denkfehler?
%Vor% Dies bezieht sich wahrscheinlich auf den Verdopplungsalgorithmus, der verwendet wird, um den Hintergrundpuffer beim Hinzufügen zu einer Liste zu verändern. Wenn Sie als Array zuweisen, ist die Länge dieses bekannt und kann abgefragt werden, indem nach IList[<T>]
und / oder ICollection[<T>]
gesucht wird; So kann es beim ersten Mal ein einzelnes Array in der richtigen Größe zuweisen und dann den Inhalt einfach blockweise kopieren.
Mit der Sequenz ist das nicht möglich (die Sequenz stellt die Länge nicht in irgendeiner zugänglichen Weise zur Verfügung); daher muss es stattdessen zurückfallen, um "den Puffer voll zu füllen; wenn es voll ist, verdopple es und kopiere".
Offensichtlich braucht das ungefähr den doppelten Speicher.
Ein interessanter Test wäre:
%Vor%Dies wird anfangs die richtige Größe zuweisen, während die Sequenz weiterhin verwendet wird.
tl; dr; Wenn der Konstruktor eine Sequenz übergeben hat, prüft er zuerst, ob er die Länge erhalten kann, indem er auf eine bekannte Schnittstelle wirft.
Dies liegt an dem Doubling-Algorithmus, der zum Erstellen des Backing-Arrays in einer Liste verwendet wird. IEnumerable verfügt über keine Count-Eigenschaft, sodass das Sicherungsarray beim Aufrufen von ToList nicht der Zielgröße zugewiesen werden kann. Tatsächlich rufen Sie bei jedem Aufruf von MoveNext ein entsprechendes Add in der Liste auf.
Array.ToList kann jedoch das Basis-ToList-Verhalten überschreiben, um die Liste mit der richtigen Kapazität zu initialisieren. Es könnte auch der List-in-it-Konstruktor sein, der versucht, die IEnumerable-Referenz, die er hat, auf bekannte Sammlungstypen wie IList, ICollection, Array usw. zu reduzieren.
Aktualisieren
Tatsächlich ist es im Konstruktor von List, dass es herausfindet, ob das Argument ICollection implementiert:
%Vor%Liste ist als ein Array implementiert. Wenn Sie das, was es zugewiesen hat, überschreiten, reserviert es ein anderes Array doppelt so groß (im Wesentlichen verdoppelt die Speicherzuweisung). Die Standardkapazität ist 4 und es verdoppelt sich von hier an.
Wenn Sie die Anzahl der Elemente auf 7.500 setzen, wird das Array wahrscheinlich auf etwas weniger als 32 MB und die IList-Größe auf 32 MB reduziert.
Sie können IList<T>
angeben, wie groß die anfängliche Größe sein soll. Wenn Sie also zum Zeitpunkt der Erstellung IEnumerable<T>
angeben, sollte nicht Speicher belegen.
[Bearbeiten] nach Kommentaren
Bei Enumerable.Range(a, b)
wird nur IEnumerable<T>
und nicht ICollection<T>
zurückgegeben. Damit List<T>
den während der Konstruktion übergebenen Artikel nicht übergibt, muss auch ein ICollection<T>
Ich denke das:
Enumerable.Range(1, 10000000)
erstellt nur ein IEnumerable und erstellt noch keine Elemente.
Enumerable.Range(1, 10000000).ToArray()
erstellt ein Array unter Verwendung von Speicher für die Zahlen
Enumerable.Range(1, 10000000).ToList()
erstellt die Nummern und zusätzliche Daten zur Verwaltung der Liste (Links zwischen Teilen. Die Liste kann ihre Größe ändern und muss Speicher in Blöcken reservieren).
Tags und Links .net c# linq memory-management