Overhead di un array di .NET?
Domanda
I cercato di determinare l'overhead dell'intestazione su un array NET (in un processo a 32 bit) utilizzando questo codice:
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();
Il risultato è stato
204800
Array overhead: 12.478
In un processo a 32 bit, un oggetto [1] dovrebbe essere la stessa dimensione come int [1], ma in realtà l'overhead salta da 3,28 byte
237568
Array overhead: 15.755
Qualcuno sa perché?
(A proposito, se qualcuno curioso, l'overhead per oggetti non matrice, ad esempio (oggetto) i nel ciclo precedente, è di circa 8 byte (8.384). Ho sentito che è 16 byte in processi a 64 bit.)
Soluzione
Ecco un programma breve ma completa un po 'più ordinato (IMO) per dimostrare la stessa cosa:
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);
}
}
Ma ottengo gli stessi risultati - l'overhead per ogni matrice tipo di riferimento è di 16 byte, mentre l'overhead per qualsiasi matrice tipo di valore è di 12 byte. Sto ancora cercando di capire perché questo è, con l'aiuto delle specifiche CLI. Non dimenticate che gli array di tipo riferimento sono covarianti, che può essere rilevante ...
EDIT: Con l'aiuto di cordbg, posso confermare la risposta di Brian - il puntatore del tipo di una matrice di riferimento di tipo è lo stesso indipendentemente dal tipo di elemento effettivo. Presumibilmente c'è qualche funky in object.GetType()
(che non è virtuale, ricordate) per tenere conto di questo.
Così, con il codice di:
object[] x = new object[1];
string[] y = new string[1];
int[] z = new int[1];
z[0] = 0x12345678;
lock(z) {}
Si finisce con qualcosa di simile al seguente:
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
Si noti che ho scaricato la parola di memoria 1 prima il valore della variabile stessa.
Per x
e y
, i valori sono:
- Il blocco di sincronizzazione, utilizzato per bloccare il codice hash (o un Blocco sottile - vedi il commento di Brian)
- tipo di puntatore
- Dimensioni di array
- Tipo elemento puntatore
- Null riferimento (primo elemento)
Per z
, i valori sono:
- blocco Sync
- tipo di puntatore
- Dimensioni di array
- 0x12345678 (primo elemento)
differenti matrici tipo di valore (byte [], int [] ecc) finiscono con diversi puntatori tipo, mentre tutti gli array del tipo di riferimento utilizzano lo stesso tipo puntatore, ma hanno un diverso tipo puntatore elemento. Il puntatore tipo di elemento è lo stesso valore come ci si trova come il puntatore del tipo per un oggetto di quel tipo. Quindi, se abbiamo guardato la memoria di un oggetto stringa nella corsa sopra, avrebbe un puntatore tipo di 0x00329134.
La parola prima che il puntatore tipo ha certamente qualcosa a che fare sia con il monitor o il codice hash: chiamando GetHashCode()
popola quel po 'di memoria, e credo che il object.GetHashCode()
predefinita ottiene un blocco di sincronizzazione per garantire unicità codice hash per tutta la vita dell'oggetto. Tuttavia, solo facendo lock(x){}
non ha fatto nulla, che mi ha sorpreso ...
Tutto ciò è valido solo per tipi "vettoriale", tra l'altro - in CLR, un tipo "vettore" è una matrice unidimensionale con un basso-bound di 0. Altri array avrà un diverso layout - per prima cosa, che avevano bisogno del limite inferiore memorizzato ...
Finora questo è stato la sperimentazione, ma ecco le congetture - il motivo per il sistema in corso di attuazione il modo in cui ha. Da qui in poi, io davvero sto solo indovinando.
- Tutti gli array
object[]
possono condividere lo stesso codice JIT. Stanno andando a comportarsi allo stesso modo in termini di allocazione di memoria, di accesso agli array, la proprietàLength
e (soprattutto) la disposizione di riferimenti per il GC. Confronti che, con array tipo di valore, in cui diversi tipi di valore possono avere diverse "impronte" GC (per esempio si potrebbe avere un byte e quindi un punto di riferimento, gli altri non avranno riferimenti a tutti, ecc). -
Ogni volta che si assegna un valore all'interno di un
object[]
il runtime deve controllare che sia valida. Ha bisogno di verificare che il tipo di oggetto il cui riferimento si sta utilizzando per il nuovo valore elemento è compatibile con il tipo di elemento della matrice. Per esempio:object[] x = new object[1]; object[] y = new string[1]; x[0] = new object(); // Valid y[0] = new object(); // Invalid - will throw an exception
Questa è la covarianza ho menzionato in precedenza. Ora dato che questo sta per accadere per ogni singolo incarico , ha senso per ridurre il numero di indirections. In particolare, ho il sospetto che non si vuole veramente far saltare in aria la cache dal dover andare al tipo di oggetto per ogni assigment per ottenere il tipo di elemento. I sospettato (e il mio assembly x86 non è abbastanza buono per verificare questo) che il test è qualcosa di simile:
- è il valore da copiare un riferimento null? Se è così, va bene. (Fatto).
- Scarica il puntatore tipo di the oggetto i punti di riferimento a.
- E 'questo tipo puntatore lo stesso del puntatore tipo di elemento (semplice controllo di uguaglianza binario)? Se è così, va bene. (Fatto).
- E 'questo tipo di puntatore assegnazione compatibile con il puntatore del tipo di elemento? (Controllo molto più complicato, con l'ereditarietà e interfacce coinvolti.) In caso affermativo, va bene -. In caso contrario, un'eccezione
Se siamo in grado di interrompere la ricerca nei primi tre passi, non c'è un sacco di riferimento indiretto - che è buono per qualcosa che sta per accadere più spesso le assegnazioni di array. Niente di tutto questo deve accadere per le assegnazioni tipo di valore, perché è staticamente verificabile.
Quindi, è per questo che credo che tipo di riferimento array sono leggermente più grande di array tipo di valore.
Grande questione - davvero interessante per approfondire esso:)
Altri suggerimenti
Array è un tipo di riferimento. Tutti i tipi di riferimento portano due campi di parole aggiuntive. Il riferimento tipo e un campo indice SyncBlock, che tra l'altro viene utilizzato per implementare serrature nel CLR. Così il tipo alto su tipi di riferimento è di 8 byte su 32 bit. In cima a che l'array memorizza anche la lunghezza che è un altro 4 byte. Questo porta l'overhead totale di 12 byte.
E ho appena appreso dalla risposta di Jon Skeet, array di tipi di riferimento ha un ulteriore 4 byte in testa. Ciò può essere confermato utilizzando WinDbg. Risulta che la parola aggiuntiva è un altro tipo di riferimento per il tipo memorizzato nella matrice. Tutti gli array di tipi di riferimento sono memorizzati internamente come object[]
, con il riferimento supplementare all'oggetto tipo di tipo effettivo. Quindi un string[]
è in realtà solo un object[]
con un tipo di ulteriore riferimento al tipo string
. Per i dettagli si prega di vedere sotto.
valori memorizzati in array: Array di tipi di riferimento tengono riferimenti agli oggetti, in modo che ogni voce nella matrice è la dimensione di un riferimento (cioè 4 byte su 32 bit). Array di tipi di valore memorizzano la linea valori e quindi ogni elemento assumerà la dimensione del tipo in questione.
Questa domanda può anche essere di interesse: C # List
Gory Dettagli
Si consideri il seguente codice
var strings = new string[1];
var ints = new int[1];
strings[0] = "hello world";
ints[0] = 42;
Collegamento WinDbg mostra il seguente:
Per prima cosa diamo un'occhiata a matrice tipo di valore.
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
Per prima cosa scarichiamo matrice e quella elemento con valore di 42. Come si può vedere la dimensione è di 16 byte. Cioè 4 byte per il valore int32
stessa, 8 byte per regolare tipo di riferimento testa e altri 4 byte per la lunghezza della matrice.
Il dump raw mostra la SyncBlock, la tabella dei metodi per int[]
, la lunghezza e il valore di 42 (2a in esadecimale). Si noti che lo SyncBlock si trova proprio di fronte il riferimento all'oggetto.
Avanti, diamo un'occhiata al string[]
per scoprire che cosa la parola aggiuntiva viene utilizzato per.
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
Per prima cosa scarichiamo la matrice e la stringa. Poi abbiamo scarichiamo le dimensioni del string[]
. Si noti che WinDbg elenca il tipo come System.Object[]
qui. Le dimensioni dell'oggetto in questo caso prevede la stringa stessa, quindi la dimensione totale è la 20 dalla matrice più il 40 per la stringa.
scarico il byte grezzi dell'istanza possiamo vedere la seguente: Innanzitutto abbiamo la SyncBlock, segue poi la tabella dei metodi per object[]
, allora la lunghezza della matrice. Dopo che troviamo i 4 byte supplementari con il riferimento alla tabella dei metodi per la stringa. Ciò può essere verificato dal comando dumpmt come mostrato sopra. Infine troviamo il singolo riferimento all'istanza stringa effettiva.
In conclusione
L'overhead per array può essere suddiviso come segue (da 32 bit che è)
- 4 byte SyncBlock
- 4 byte per tabella Metodo (tipo di riferimento) per la matrice stessa
- 4 byte per la lunghezza di array
- Arrays di tipi di riferimento aggiunge altri 4 byte per contenere la tabella procedimento del tipo elemento effettivo (tipo array di riferimento sono
object[]
sotto il cofano)
vale a dire. l'overhead è 12 byte per matrici di tipo valore e 16 byte per gli array del tipo di riferimento .
Credo che si stanno facendo alcune ipotesi errate durante la misurazione, come l'allocazione di memoria (via GetTotalMemory) durante il ciclo può essere diverso da quello della memoria effettiva necessaria solo per gli array - la memoria può essere allocata in blocchi più grandi, ci possono essere altri oggetti in memoria che sono recuperati durante il ciclo, ecc.
Ecco alcune informazioni per voi su array di testa:
Poiché la gestione mucchio (dal momento che si tratta con GetTotalMemory) può allocare solo piuttosto grandi blocchi, che queste ultime vengono assegnati da blocchi più piccoli per scopi programmatore dal CLR.
Mi dispiace per l'offtopic ma ho trovato informazioni interessanti sulla memoria overheading solo la mattina di oggi.
Abbiamo un progetto che opera enorme quantità di dati (fino a 2GB). In qualità di principale stoccaggio usiamo Dictionary<T,T>
. Migliaia di dizionari vengono creati in realtà. Dopo modificarlo per List<T>
per le chiavi e List<T>
per i valori (abbiamo implementato stessi IDictionary<T,T>
) l'utilizzo della memoria è diminuita in circa il 30-40%.
Perché?