it-swarm.com.de

Speicherbelegungsunterschied zwischen generischen und nicht generischen Auflistungen in .NET

Ich lese heutzutage über Sammlungen in .NET. Bekanntlich gibt es einige Vorteile bei der Verwendung von generischen Sammlungen gegenüber nicht generischen : Sie sind typensicher und es gibt kein Casting, kein Boxing/Unboxing. Deshalb haben generische Kollektionen einen Leistungsvorteil.

Wenn wir davon ausgehen, dass nicht generische Auflistungen jedes Mitglied als object speichern, können wir davon ausgehen, dass Generika auch einen Speichervorteil haben. Ich habe jedoch keine Informationen über den Unterschied in der Speichernutzung gefunden.

Kann jemand über den Punkt klären?

5
İlkin Elimov

Wenn wir berücksichtigen, dass nicht generische Auflistungen jedes Mitglied als Objekt speichern, können wir annehmen, dass Generika auch einen Speichervorteil haben. Ich habe jedoch keine Informationen über den Unterschied in der Speichernutzung gefunden. Kann jemand über den Punkt klären?

Sicher. Betrachten wir eine ArrayList, die ints gegen ein List<int> enthält. Nehmen wir an, es gibt 1000 ints in jeder Liste.

In beiden Fällen ist der Auflistungstyp eine dünne Hülle um ein Array - daher der Name ArrayList. Im Fall von ArrayList gibt es ein zugrunde liegendes object[], das mindestens 1000 Boxed Ints enthält. Im Fall von List<int> gibt es ein zugrunde liegendes int[], das mindestens 1000 ints enthält.

Warum habe ich "zumindest" gesagt? Weil beide eine Double-When-Full-Strategie anwenden. Wenn Sie die Kapazität einer Sammlung beim Erstellen festlegen, wird genügend Speicherplatz für so viele Dinge zugewiesen. Wenn Sie dies nicht tun, muss die Sammlung raten, und wenn sie falsch raten und Sie mehr Kapazität benötigen, verdoppelt sie ihre Kapazität. Im besten Fall haben unsere Sammlungsarrays also genau die richtige Größe. Im schlimmsten Fall sind sie möglicherweise doppelt so groß wie sie sein müssen; In den Arrays könnte Platz für 2000 Objekte oder 2000 Zoll sein.

Nehmen wir der Einfachheit halber an, wir haben Glück und es sind ungefähr 1000 in jedem.

Was ist zu Beginn die Speicherlast nur des Arrays? Ein object[1000] belegt auf einem 32-Bit-System 4000 Byte und auf einem 64-Bit-System 8000 Byte, nur für die Verweise, die Zeigergröße haben. Ein int[1000] belegt unabhängig davon 4000 Bytes. (Es gibt auch einige zusätzliche Bytes, die von der Array-Buchhaltung belegt werden, aber diese Kosten sind im Vergleich zu den Grenzkosten gering.)

Wir sehen also bereits, dass die nicht generische Lösung möglicherweise nur für das Array doppelt so viel Speicher benötigt. Was ist mit dem Inhalt des Arrays?

Nun, die Sache mit Werttypen ist sie werden genau dort in ihrer eigenen Variablen gespeichert . Außer den 4000 Bytes, die zum Speichern der 1000 Ganzzahlen verwendet werden, ist kein zusätzlicher Speicherplatz vorhanden. Sie werden direkt in das Array gepackt. Die zusätzlichen Kosten für den generischen Fall sind also Null.

Für den Fall object[] ist jedes Mitglied des Arrays eine Referenz, und diese Referenz bezieht sich auf ein Objekt. In diesem Fall eine Ganzzahl in einem Kästchen. Wie groß ist eine geschachtelte Ganzzahl?

Ein Werttyp ohne Box muss keine Informationen über seinen Typ speichern, da sein Typ durch den Typ des Speichers bestimmt wird, in dem er sich befindet, und der der Laufzeit bekannt ist. Ein Boxed-Value-Typ muss irgendwo den Typ des Objekts in der Box speichern, und das braucht Platz. Es stellt sich heraus, dass der Buchhaltungsaufwand für ein Objekt in 32-Bit-.NET 8 Byte und auf 64-Bit-Systemen 16 Byte beträgt. Das ist nur der Aufwand; wir brauchen natürlich 4 bytes für den int. Aber warte, es wird schlimmer: Auf 64-Bit-Systemen muss die Box an einer 8-Byte-Grenze ausgerichtet sein, sodass wir auf 64-Bit-Systemen eine weitere 4 Byte Auffüllung benötigen.

Add it all up: Unser int[] benötigt auf 64- und 32-Bit-Systemen ungefähr 4 KB. Unser object[] mit 1000 Zoll benötigt auf 32-Bit-Systemen ungefähr 16 KB und auf 64-Bit-Systemen 32 KB. Daher ist die Speichereffizienz eines int[] im Vergleich zu einem object[] für den nicht generischen Fall entweder vier- oder achtmal schlechter.

Aber warte, es wird noch schlimmer. Das ist nur Größe. Was ist mit der Zugriffszeit?

Um auf eine Ganzzahl aus einem Array von Ganzzahlen zuzugreifen, muss die Laufzeitumgebung:

  • stellen Sie sicher, dass das Array gültig ist
  • stellen Sie sicher, dass der Index gültig ist
  • ruft den Wert aus der Variablen am angegebenen Index ab

Um auf eine Ganzzahl aus einem Array von Ganzzahlen in Boxen zuzugreifen, muss die Laufzeitumgebung:

  • stellen Sie sicher, dass das Array gültig ist
  • stellen Sie sicher, dass der Index gültig ist
  • rufen Sie die Referenz aus der Variablen am angegebenen Index ab
  • stellen Sie sicher, dass die Referenz nicht null ist
  • stellen Sie sicher, dass es sich bei der Referenz um eine Ganzzahl in Box handelt
  • extrahieren Sie die ganze Zahl aus dem Feld

Das sind viel mehr Schritte, es dauert also viel länger.

ABER WARTEN, DASS ES SCHLECHT WIRD.

Moderne Prozessoren verwenden Caches auf dem Chip selbst, um nicht in den Hauptspeicher zurückzukehren. Ein Array mit 1000 einfachen Ganzzahlen wird höchstwahrscheinlich im Cache gespeichert, sodass Zugriffe auf die ersten, zweiten, dritten usw. Mitglieder des Arrays in schneller Folge alle aus derselben Cache-Zeile abgerufen werden. das ist wahnsinnig schnell . Ganzzahlen in Boxen können sich jedoch überall auf dem Heap befinden, was die Cache-Fehler erhöht und den Zugriff noch weiter verlangsamt.

Hoffentlich wird dadurch Ihr Verständnis der Boxstrafe ausreichend geklärt.

Was ist mit nicht verpackten Typen? Gibt es einen signifikanten Unterschied zwischen einer Array-Liste von Zeichenfolgen und einem List<string>?

Hier ist der Nachteil sehr viel geringer, da ein object[] und ein string[] ähnliche Leistungseigenschaften und Speicherlayouts aufweisen. Die einzige zusätzliche Strafe in diesem Fall besteht darin, (1) Ihre Fehler erst zur Laufzeit abzufangen, (2) das Lesen und Bearbeiten des Codes zu erschweren und (3) die geringfügige Strafe einer Laufzeit-Typprüfung.

18
Eric Lippert

dann können wir denken, dass Generika auch Gedächtnisvorteile haben

Diese Annahme ist falsch, sie gilt nur für Werttypen. Betrachten Sie also Folgendes:

new ArrayList { 1, 2, 3 };

Dadurch wird implizit jede Ganzzahl in object (als Boxing bezeichnet) umgewandelt, um sie in Ihrem ArrayList zu speichern. Dies wird Ihren Speicher-Overhead hier verursachen, da ein object sicherlich größer ist als ein einfaches int.

Für Referenztypen gibt es jedoch keinen Unterschied, da kein Boxen erforderlich ist.

Das Verwenden des einen oder anderen sollte weder mit Performance- noch mit Speicherproblemen verbunden sein. Sie sollten sich jedoch fragen, was Sie mit den Ergebnissen anfangen möchten. Insbesondere wenn Sie die Typen kennen, die zum Zeitpunkt der Kompilierung in Ihrer Sammlung gespeichert sind, gibt es keinen Grund, diese Informationen nicht mithilfe des Rechts in den Kompilierungsprozess einzubeziehen generisches Typ-Argument.

Auf jeden Fall sollten Sie wegen der erwähnten Typensicherheit immer generische Sammlungen anstelle von nicht generischen verwenden.

BEARBEITEN: Ihre eigentliche Frage, ob Sie eine nicht-generische Sammlung oder eine generische Version verwenden, ist völlig sinnlos: Verwenden Sie immer die generische. Aber nicht wegen der Speichernutzung. Sieh dir das an:

ArrayList a = new ArrayList { 1, 2, 3};

vs.

List<object> a = new List<object> { 1, 2, 3 };

Beide Listen belegen den gleichen Speicherplatz, obwohl die zweite generisch ist. Das liegt daran, dass sie beide Ihre ganzen Zahlen in object setzen. Die Antwort auf die Frage hat also nichts mit dem Gedächtnis zu tun.

Zum anderen gibt es für Referenztypen überhaupt keinen Speicherunterschied:

ArrayList a = new ArrayList { myInstance, anotherInstance }

vs.

List<MyClass> a = new List<MyClass> { myInstance, anotherInstance }

erzeugt das gleiche Gedächtnisergebnis. Die zweite ist jedoch weitaus einfacher zu warten, da Sie direkt mit den Instanzen arbeiten können, ohne sie zu übertragen.

2
HimBromBeere

Nehmen wir an, wir haben diese Aussage:

int valueType = 1;

jetzt haben wir einen Wert auf dem Stapel, der wie folgt lautet:

stapel

i = 1

Jetzt überlegen wir uns, ob wir das jetzt tun:

object boxingObject = valueType;

Jetzt haben wir zwei Werte im Speicher, die Referenz für valueType im Stack und den value 1 im Heap:

stapel

boxingObject

haufen

1

Wenn Sie also einen Wertetyp einschränken, wird zusätzlicher Speicherplatz benötigt, wie in Microsoft Docs angegeben:

Beim Einrahmen eines Wertetyps wird eine Objektinstanz auf dem Heap zugeordnet und der Wert in das neue Objekt kopiert.

Siehe diesen Link für vollständige Informationen.

0
Ali Ezzat Odeh