Overhead eines NET-Array?
Frage
Ich habe versucht, den Aufwand des Headers auf einem .NET-Array (in einem 32-Bit-Prozess) mit diesem Code zu bestimmen:
long bytes1 = GC.GetTotalMemory(false);
object[] array = new object[10000];
for (int i = 0; i < 10000; i++)
array[i] = new int[1];
long bytes2 = GC.GetTotalMemory(false);
array[0] = null; // ensure no garbage collection before this point
Console.WriteLine(bytes2 - bytes1);
// Calculate array overhead in bytes by subtracting the size of
// the array elements (40000 for object[10000] and 4 for each
// array), and dividing by the number of arrays (10001)
Console.WriteLine("Array overhead: {0:0.000}",
((double)(bytes2 - bytes1) - 40000) / 10001 - 4);
Console.Write("Press any key to continue...");
Console.ReadKey();
Das Ergebnis war
204800
Array overhead: 12.478
in einem 32-Bit-Verfahren, das Gegenstand [1] soll die gleiche Größe wie int [1], aber in Wirklichkeit der Overhead-Sprünge von 3,28 Bytes
237568
Array overhead: 15.755
Wer weiß, warum?
(Übrigens, wenn jemand die gespannt, der Overhead für Nicht-Array-Objekte, zB (Objekt) i in der Schleife oben, über 8 Bytes ist (8,384). I gehört, es ist 16 Bytes in 64-Bit-Prozesse).
Lösung
Hier ist ein etwas sauberere (IMO) kurzes, aber vollständiges Programm, das Gleiche zeigen:
using System;
class Test
{
const int Size = 100000;
static void Main()
{
object[] array = new object[Size];
long initialMemory = GC.GetTotalMemory(true);
for (int i = 0; i < Size; i++)
{
array[i] = new string[0];
}
long finalMemory = GC.GetTotalMemory(true);
GC.KeepAlive(array);
long total = finalMemory - initialMemory;
Console.WriteLine("Size of each element: {0:0.000} bytes",
((double)total) / Size);
}
}
Aber ich bekomme die gleichen Ergebnisse - der Aufwand für jeden Referenztyp Array ist 16 Bytes, während der Aufwand für einen beliebigen Wert Typ Array 12 Bytes ist. Ich versuche immer noch herauszufinden, warum das so ist, mit Hilfe der CLI-Spezifikation. Vergessen Sie nicht, dass die Referenztyp-Arrays sind covariant, die relevant sein können ...
EDIT: Mit Hilfe von cordbg kann ich Brians Antwort bestätigen - der Typ Zeiger eines Referenz-Typ Array das gleiche ist, unabhängig von dem tatsächlichen Elementtyp. Vermutlich gibt es einige funkiness in object.GetType()
(die nicht virtuell ist, erinnern) zur Rechenschaft für diese.
Also, mit Code:
object[] x = new object[1];
string[] y = new string[1];
int[] z = new int[1];
z[0] = 0x12345678;
lock(z) {}
Wir arbeiten mit so etwas wie die folgenden am Ende:
Variables:
x=(0x1f228c8) <System.Object[]>
y=(0x1f228dc) <System.String[]>
z=(0x1f228f0) <System.Int32[]>
Memory:
0x1f228c4: 00000000 003284dc 00000001 00326d54 00000000 // Data for x
0x1f228d8: 00000000 003284dc 00000001 00329134 00000000 // Data for y
0x1f228ec: 00000000 00d443fc 00000001 12345678 // Data for z
Beachten Sie, dass ich den Speicher 1 Wort abgeladen haben vor der Wert der Variablen selbst.
Für x
und y
, die Werte sind:
- Der Sync-Block, der für den Hash-Code Sperren (oder eine dünne Sperre - siehe Brians Kommentar)
- Typ Zeiger
- Size of array
- Elementtyp Zeiger
- NULL (erstes Element)
Für z
, die Werte sind:
- Sync-Block
- Typ Zeiger
- Size of array
- 0x12345678 (erstes Element)
anderen Wert Typ-Arrays (byte [], int [] etc) am Ende mit anderen Typ Zeiger, während alle Referenztyp-Arrays die gleiche Art Zeiger verwenden, haben jedoch einen anderen Elementtyp-Zeiger. Der Elementtyp Zeiger ist der gleiche Wert wie Sie als Typ Zeiger für ein Objekt dieser Art finden würden. Wenn wir also in einem String-Objekt Speicher in denen oben laufen sehen, wäre es eine Art Zeiger von 0x00329134 hat.
Das Wort vor dem Typ Zeiger hat sicherlich etwas zu tun mit entweder dem Monitor oder dem Hash-Code: Aufruf GetHashCode()
bevölkert, dass wenig Speicher, und ich glaube, das Standard-object.GetHashCode()
erhält einen Sync-Block, um sicherzustellen, Hash-Code Einzigartigkeit für die gesamte Lebensdauer des Objekts. Allerdings nur lock(x){}
tun hat nichts getan, was überrascht mich ...
All dies gilt nur für „Vektor“ Typen, nebenbei gesagt - in der CLR, ein „Vektor“ Typ ist ein eindimensionales Array mit einer Untergrenze von 0. Anderen Arrays wird ein anderes Layout hat - für eine Sache, bräuchten sie die untere gespeichert bound ...
Bisher hat dieses Experiment gewesen, aber hier ist die Vermutung - der Grund für das System die Art und Weise umgesetzt hat. Von hier aus bin Erraten ich wirklich nur.
- Alle
object[]
Arrays können den gleichen JIT-Code teilen. Sie werden auf die gleiche Weise in Bezug auf die Speicherzuweisung, Array-Zugriff,Length
Eigentum und (wichtiger) das Layout der Referenzen für die GC verhalten. Vergleichen Sie das mit dem Werttyp-Arrays, in denen unterschiedliche Werttypen unterschiedliche GC „Fußabdrücke“ haben kann (zum Beispiel könnte man ein Byte haben und dann einen Verweis, andere werden keine Verweise auf alle haben, etc). -
Jedes Mal, wenn Sie einen Wert innerhalb eines
object[]
zuweisen die Laufzeitanforderungen zu überprüfen, ob es gültig ist. Es muss prüfen, ob die Art des Objekts, deren Referenz Sie für die neue Element Wert Verwendung mit dem Elementtyp des Arrays kompatibel ist. Zum Beispiel:object[] x = new object[1]; object[] y = new string[1]; x[0] = new object(); // Valid y[0] = new object(); // Invalid - will throw an exception
Dies ist die Kovarianz ich bereits erwähnt. Nun da dies für passieren wird jede einzelne Zuordnung , macht es Sinn, die Anzahl der Indirekt zu reduzieren. Insbesondere ich vermute, dass Sie nicht wirklich wollen, um den Cache blasen durch für jeden assigment des Typs Objekt gelingt mit dem Elementtyp zu erhalten. I verdächtigt (und meine x86-Assembler nicht gut genug ist, um dies zu überprüfen), dass der Test ist so etwas wie:
- Ist der Wert auf einen NULL-Verweis kopiert werden? Wenn ja, ist das in Ordnung. (Fertig).
- Holt den Typ Zeiger von the-Objekt der Referenzpunkte an.
- Ist dieser Typ Zeiger gleich den Elementtyp Zeiger (einfache binäre Gleichheitsprüfung)? Wenn ja, ist das in Ordnung. (Fertig).
- Ist dieser Typ Zeiger zuweisungskompatibel mit dem Elementtyp Zeiger? (Viel komplizierter zu überprüfen, mit Vererbung und beteiligten Schnittstellen.) Wenn ja, das ist in Ordnung -. Andernfalls eine Ausnahme aus,
Wenn wir die Suche in den ersten drei Schritten beenden können, gibt es nicht viel indirection - was gut für etwas, das so oft wie Array-Zuweisungen passieren wird. Keine dieser Anforderungen für Wertzuweisungen passieren, denn das statisch überprüfbar ist.
Also, das ist, warum ich Referenzart Arrays glauben sind etwas größer als Werttyp-Arrays.
Gute Frage - wirklich interessant zu tauchen Sie ein in es:)
Andere Tipps
Array ist ein Referenztyp. Alle Referenztypen tragen zwei weitere Wortfelder. Die Typenbezeichnung und ein Synchronisationsblock Indexfeld, das unter anderem verwendet wird, Sperre in der CLR zu implementieren. So ist die Art Overhead auf Referenztypen ist 8 Byte auf 32 Bit. Hinzu kommt, dass das Array selbst speichert auch die Länge, die noch 4 Bytes. Dies bringt die Gesamtoverhead zu 12 Byte.
Und ich gerade von Jon Skeet Antwort gelernt, Arrays von Referenztypen hat zusätzlich 4 Byte-Overhead. Dies kann unter Verwendung von WinDbg bestätigt werden. Es stellt sich heraus, dass das zusätzliche Wort eine andere Art Referenz für den Typ in dem Array gespeichert ist. Alle Arrays von Referenztypen werden intern als object[]
, mit dem zusätzlichen Hinweis auf die Art des tatsächlichen Objekt-Typs gespeichert. So ein string[]
ist wirklich nur ein object[]
mit einem zusätzlichen Typ Bezug auf den Typ string
. Für Details siehe unten.
gespeicherten Werte in dem Arrays: Arrays von Referenztypen enthalten Referenzen auf Objekte, so dass jeder Eintrag in dem Array ist in der Größe einer Referenz (d.h. 4 Bytes auf 32 Bit). Arrays von Werttypen speichert die Werte inline und damit jedes Element wird die Größe der in Rede stehenden Art in Anspruch nehmen.
Diese Frage auch von Interesse sein können: C # List
Gory Details
Betrachten Sie den folgenden Code
var strings = new string[1];
var ints = new int[1];
strings[0] = "hello world";
ints[0] = 42;
Ansetztechnologie WinDbg zeigt folgendes:
Lassen Sie uns zunächst einen Blick auf den Wert Typ Array.
0:000> !dumparray -details 017e2acc
Name: System.Int32[]
MethodTable: 63b9aa40
EEClass: 6395b4d4
Size: 16(0x10) bytes
Array: Rank 1, Number of elements 1, Type Int32
Element Methodtable: 63b9aaf0
[0] 017e2ad4
Name: System.Int32
MethodTable 63b9aaf0
EEClass: 6395b548
Size: 12(0xc) bytes
(C:\Windows\assembly\GAC_32\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll)
Fields:
MT Field Offset Type VT Attr Value Name
63b9aaf0 40003f0 0 System.Int32 1 instance 42 m_value <=== Our value
0:000> !objsize 017e2acc
sizeof(017e2acc) = 16 ( 0x10) bytes (System.Int32[])
0:000> dd 017e2acc -0x4
017e2ac8 00000000 63b9aa40 00000001 0000002a <=== That's the value
Zuerst haben wir Dump das Array und das ein Element mit einem Wert von 42 kann die Größe ist 16 Byte zu sehen. Das ist 4 Bytes für den Wert selbst int32
, 8 Bytes für die regelmäßigen Referenztyp-Overhead und weitere 4 Byte für die Länge des Arrays.
unbe Dump zeigt den Synchronisationsblock, die Methodentabelle für int[]
, die Länge, und der Wert von 42 (2a in hex). Beachten Sie, dass der Synchronisationsblock direkt vor dem Objekt Referenz befindet.
Als nächstes wollen wir einen Blick auf die string[]
um herauszufinden, was das zusätzliche Wort verwendet wird.
0:000> !dumparray -details 017e2ab8
Name: System.String[]
MethodTable: 63b74ed0
EEClass: 6395a8a0
Size: 20(0x14) bytes
Array: Rank 1, Number of elements 1, Type CLASS
Element Methodtable: 63b988a4
[0] 017e2a90
Name: System.String
MethodTable: 63b988a4
EEClass: 6395a498
Size: 40(0x28) bytes <=== Size of the string
(C:\Windows\assembly\GAC_32\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll)
String: hello world
Fields:
MT Field Offset Type VT Attr Value Name
63b9aaf0 4000096 4 System.Int32 1 instance 12 m_arrayLength
63b9aaf0 4000097 8 System.Int32 1 instance 11 m_stringLength
63b99584 4000098 c System.Char 1 instance 68 m_firstChar
63b988a4 4000099 10 System.String 0 shared static Empty
>> Domain:Value 00226438:017e1198 <<
63b994d4 400009a 14 System.Char[] 0 shared static WhitespaceChars
>> Domain:Value 00226438:017e1760 <<
0:000> !objsize 017e2ab8
sizeof(017e2ab8) = 60 ( 0x3c) bytes (System.Object[]) <=== Notice the underlying type of the string[]
0:000> dd 017e2ab8 -0x4
017e2ab4 00000000 63b74ed0 00000001 63b988a4 <=== Method table for string
017e2ac4 017e2a90 <=== Address of the string in memory
0:000> !dumpmt 63b988a4
EEClass: 6395a498
Module: 63931000
Name: System.String
mdToken: 02000024 (C:\Windows\assembly\GAC_32\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll)
BaseSize: 0x10
ComponentSize: 0x2
Number of IFaces in IFaceMap: 7
Slots in VTable: 196
Zuerst haben wir Dump das Array und die Zeichenfolge. Als nächstes werden wir Dump die Größe des string[]
. Beachten Sie, dass hier WinDbg den Typ als System.Object[]
auflistet. Die Objektgröße in diesem Fall enthält die Zeichenfolge selbst, so dass die Gesamtgröße die 20 von dem Array 40 sowie die für die Zeichenkette.
Durch das rohe Bytes der Instanz Dumping wir folgendes sehen: Zuerst haben wir den Synchronisationsblock haben, folgt dann die Methodentabelle für object[]
, dann die Länge des Arrays. Danach haben wir die zusätzlichen 4 Bytes mit dem Verweis auf die Methodentabelle für Zeichenfolge finden. Dies kann durch den dumpmt Befehl überprüft werden, wie oben gezeigt. Schließlich wir den einzigen Hinweis auf die tatsächliche String-Instanz finden.
Fazit
Der Overhead für Arrays können aufgeschlüsselt werden, wie folgt (auf 32 Bit, das ist)
- 4 Byte Synchronisationsblock
- 4 Byte für Methodentabelle (Typ Referenz) für das Array selbst
- 4 Byte für Länge des Arrays
- Arrays von Referenztypen fügt weitere 4 Bytes der Methodentabelle der aktuellen Elementtyp (Referenztyp-Arrays sind
object[]
unter der Motorhaube) zu halten,
d. der Aufwand ist 12 Bytes für Werttyp Arrays und 16 Bytes für Arrays Referenztyp .
Ich glaube, Sie einige fehlerhafte Annahmen machen während der Messung, da die Speicherzuweisung (über GetTotalMemory) während der Schleife unterschiedlich sein können als die tatsächliche benötigte Speicher nur für die Arrays - kann der Speicher in größeren Blöcken zugeordnet werden, kann es sein, andere Objekte im Speicher, die während der Schleife zurückgewonnen werden, etc.
Hier einige Informationen für Sie auf Array-Overhead:
Da Heapverwaltung (da Sie mit GetTotalMemory beschäftigen) nur zuweisen ziemlich große Blöcke, die durch kleinere Stücke für Programmierer Zwecke von CLR zugewiesen Letztere werden.
Es tut mir leid für die offtopic, aber ich fand interessante Informationen über Speicher overheading gerade heute Morgen.
Wir haben ein Projekt, das große Datenmengen arbeitet (bis zu 2 GB). Als Hauptspeicher verwenden Dictionary<T,T>
wir. Tausende von Wörterbüchern tatsächlich erstellt. Nach Änderung es List<T>
für Schlüssel und List<T>
für Werte (wir umgesetzt IDictionary<T,T>
sich) die Speicherauslastung etwa 30-40% verringert werden.
Warum?