Come posso controllare il numero di byte consumati da una struttura?

Se sto creando una struttura relativamente grande, come posso calcolare i byte che occupa in memoria?

Possiamo farlo manualmente, ma se la struttura è abbastanza grande, come facciamo? C’è qualche pezzo di codice o applicazione?

È ansible utilizzare l’operatore sizeof o la funzione SizeOf .
Ci sono alcune differenze tra queste opzioni, vedi i link di riferimento per maggiori informazioni.

Ad ogni modo un buon modo per usare quella funzione è avere un metodo generico o un metodo di estensione come questi:

 static class Test { static void Main() { //This will return the memory usage size for type Int32: int size = SizeOf(); //This will return the memory usage size of the variable 'size': //Both lines are basically equal, the first one makes use of ex. methods size = size.GetSize(); size = GetSize(size); } public static int SizeOf() { return System.Runtime.InteropServices.Marshal.SizeOf(typeof(T)); } public static int GetSize(this object obj) { return System.Runtime.InteropServices.Marshal.SizeOf(obj); } } 

Le strutture sono state bestie fastidiose in ingegneria informatica per un tempo molto lungo. Il loro layout di memoria dipende molto dall’hardware. Per renderli efficienti, i loro membri devono essere allineati in modo che la CPU possa leggere e scrivere rapidamente i loro valori senza dover moltiplicare i byte per adattarsi alla larghezza del bus di memoria. Ogni compilatore ha una propria strategia di imballaggio dei membri, spesso diretta, ad esempio, dalla direttiva #pragma pack in un programma C o C ++.

Che va bene, ma piuttosto un problema negli scenari di interoperabilità. Dove un pezzo di codice può fare ipotesi diverse sul layout della struttura rispetto a un altro blocco, compilato da un compilatore diverso. Potete vedere questo in COM, la soluzione di nonno di .NET per interoperare con la programmazione. COM ha un supporto molto scarso per la gestione delle strutture. Non li supporta come un tipo di automazione nativo ma ha una soluzione alternativa tramite l’interfaccia IRecordInfo. Che consente a un programma di rilevare il layout della memoria in fase di esecuzione tramite una dichiarazione esplicita della struttura in una libreria di tipi. Che funziona bene, ma è piuttosto inefficiente.

I progettisti .NET hanno preso una decisione molto coraggiosa e corretta per risolvere questo problema. Hanno reso il layout di memoria di una struttura completamente non individuabile. Non esiste un modo documentato per recuperare l’offset di un membro. E per estensione, non c’è modo di scoprire la dimensione della struttura. La risposta preferita di tutti, utilizzare Marshal.SizeOf () non è in realtà la soluzione. Ciò restituisce la dimensione della struct dopo che è stato eseguito il marshalling , la dimensione che sarebbe necessario passare a, ad esempio, Marshal.AllocCoTaskMem () prima di chiamare Marshal.StructureToPtr. Questo sistema e allinea i membri della struct in base all’attributo [StructLayout] associato alla struct. Si noti che questo attributo non è richiesto per le struct (come per le classi), il runtime implementa un default che usa l’ordine dichiarato per i membri.

Un effetto collaterale molto bello del layout è irrintracciabile è che il CLR può giocare brutti scherzi con esso. Quando si impacchettano i membri della struttura e li si allinea, il layout può ottenere fori che non memorizzano alcun dato. Chiamato padding byte. Dato che il layout non è rintracciabile, il CLR può effettivamente utilizzare il padding. Muove un membro se è abbastanza piccolo da adattarsi a tale foro. Ora otterrai una struttura la cui dimensione è inferiore a quella che sarebbe normalmente richiesta in base al layout della struttura dichiarato. In particolare, Marshal.SizeOf () restituirà il valore errato per la dimensione della struttura, restituisce un valore troppo grande.

Per farla breve, non esiste un modo generale per ottenere un valore preciso per la dimensione della struttura a livello di codice. La cosa migliore da fare è semplicemente non porre la domanda. Marshal.SizeOf () ti darà un’ipotesi, assumendo che la struttura sia blittabile. Se è necessario un valore preciso per qualche motivo, è ansible esaminare il codice macchina generato di un metodo che dichiara una variabile locale del tipo di struttura e confrontarlo con lo stesso metodo senza quella variabile locale. Vedrai la differenza nella regolazione del puntatore dello stack, l’istruzione “sub esp, xxx” nella parte superiore del metodo. Naturalmente, dipenderà dall’architettura, in genere si otterrà una struttura più grande in modalità 64 bit.

È ansible utilizzare la parola chiave sizeof() per le strutture definite dall’utente che non contengono campi o proprietà che sono tipi di riferimento o utilizzare Marshal.SizeOf(Type) o Marshal.SizeOf(object) per ottenere la dimensione non gestita di un scrivi o una struttura che ha un layout sequenziale o esplicito.

Ho scritto una piccola libreria in CIL (linguaggio assembly di .NET ) per esporre alcune funzionalità pulite che non sono disponibili in C #. Ho rotto la dimensione delle istruzioni.

Si differenzia significativamente dall’operatore sizeof in C #. Fondamentalmente, ottiene la dimensione di una struttura (o di un tipo di riferimento, che agisce in modo divertente con alcune ottimizzazioni) incluso il riempimento e tutto. Quindi, se si dovesse creare un array di T , è ansible utilizzare sizeof per determinare la distanza tra ciascun elemento dell’array in byte . È anche codice completamente verificabile e gestito. Si noti che in Mono c’era un bug (pre-3.0?) Che avrebbe causato la dimensione errata dei tipi di riferimento da riportare in modo errato, che si espanderebbe in strutture contenenti tipi di riferimento.

Ad ogni modo, è ansible scaricare la libreria con licenza BSD (e CIL) da BitBucket . Puoi anche vedere alcuni esempi di codice e qualche altro dettaglio nel mio blog .

Si desidera utilizzare System.Runtime.InteropServices.Marshal.SizeOf () :

 struct s { public Int64 i; } public static void Main() { s s1; s1.i = 10; var s = System.Runtime.InteropServices.Marshal.SizeOf(s1); } 

In .NET Core , la dimensione dell’istruzione CIL è stata esposta tramite la class Unsafe aggiunta di recente. Aggiungi un riferimento al pacchetto System.Runtime.CompilerServices.Unsafe e fai semplicemente questo:

 int size = Unsafe.SizeOf(); 

Funziona anche per i tipi di riferimento (restituirà 4 o 8 a seconda dell’architettura del tuo computer).

È ansible utilizzare System.Runtime.InteropServices.Marshal.SizeOf() per ottenere anche le dimensioni in byte.

0. Per il codice di esempio:

 using System; using System.Diagnostics; using System.Reflection.Emit; using System.Runtime.InteropServices; 

1. Struttura dimostrativa

 [Serializable, StructLayout(LayoutKind.Sequential, Pack = 128)] public struct T { public int a; public byte b; public int c; public String d; public short e; }; 

2. Sottraendo i puntatori gestiti:

 /// Return byte-offset between managed addresses of struct instances 'hi' and 'lo' public static long IL.RefOffs(ref T1 hi, ref T2 lo) { ... } 
 public static class IL { public delegate long _ref_offs(ref T1 hi, ref T2 lo); public static readonly _ref_offs RefOffs; static IL() { var dm = new DynamicMethod( Guid.NewGuid().ToString(), typeof(long), new[] { typeof(T1).MakeByRefType(), typeof(T2).MakeByRefType() }, typeof(Object), true); var il = dm.GetILGenerator(); il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Ldarg_1); il.Emit(OpCodes.Sub); il.Emit(OpCodes.Conv_I8); il.Emit(OpCodes.Ret); RefOffs = (_ref_offs)dm.CreateDelegate(typeof(_ref_offs)); } }; 

3. Mostra il layout della struttura interna gestita:

 static class demonstration { /// Helper thunk that enables automatic type-inference from argument types static long RefOffs(ref T1 hi, ref T2 lo) => IL.RefOffs(ref hi, ref lo); public static void Test() { var t = default(T); var rgt = new T[2]; Debug.Print("Marshal.Sizeof: {0,2}", Marshal.SizeOf()); Debug.Print("&rgt[1] - &rgt[0]: {0,2}", RefOffs(ref rgt[1], ref rgt[0])); Debug.Print("int &t.a {0,2}", RefOffs(ref ta, ref t)); Debug.Print("byte &t.b {0,2}", RefOffs(ref tb, ref t)); Debug.Print("int &t.c {0,2}", RefOffs(ref tc, ref t)); Debug.Print("String &t.d {0,2}", RefOffs(ref td, ref t)); Debug.Print("short &t.e {0,2}", RefOffs(ref te, ref t)); } }; 

4. Risultati e discussione

L’ StructLayout(..., Pack) può essere aggiunta alla dichiarazione di struct T con uno dei seguenti valori: {0, 1, 2, 4, 8, 16, 32, 64, 128} . Il valore predefinito quando Pack non è specificato – o equivalentemente con Pack=0 set di pacchetti uguale a IntPtr.Size ( 4 su x86, 8 su x64).

I risultati dell’esecuzione del programma precedente mostrano che il valore di Pack influisce solo sulla dimensione di marshalling riportata da Marshal.SizeOf e non sulla dimensione effettiva di una singola immagine di memoria T , che si presume sia l’offset di byte tra istanze fisicamente adiacenti. Il codice di test lo misura tramite array diagnostico gestito new T[2] assegnato a rgt .

========= x86 ========== ========= x64 ==========

-------- Pack=1 -------- -------- Pack=1 --------
Marshal.Sizeof(): 15 Marshal.Sizeof(): 19
&rgt[1] - &rgt[0]: 16 &rgt[1] - &rgt[0]: 24

-------- Pack=2 -------- -------- Pack=2 --------
Marshal.Sizeof(): 16 Marshal.Sizeof(): 20
&rgt[1] - &rgt[0]: 16 &rgt[1] - &rgt[0]: 24

--- Pack=4/0/default --- -------- Pack=4 --------
Marshal.Sizeof(): 20 Marshal.Sizeof(): 24
&rgt[1] - &rgt[0]: 16 &rgt[1] - &rgt[0]: 24

-------- Pack=8 -------- --- Pack=8/0/default ---
Marshal.Sizeof(): 20 Marshal.Sizeof(): 32
&rgt[1] - &rgt[0]: 16 &rgt[1] - &rgt[0]: 24

-- Pack=16/32/64/128 --- -- Pack=16/32/64/128 ---
Marshal.Sizeof(): 20 Marshal.Sizeof(): 32
&rgt[1] - &rgt[0]: 16 &rgt[1] - &rgt[0]: 24

Come notato, troviamo che per architettura ( x86 , x64 ), il layout del campo gestito è coerente indipendentemente dall’impostazione del Pack . Ecco gli attuali offset di campo gestiti, sempre per la modalità a 32 e 64 bit, come riportato dal codice sopra:

┌─offs─┐
field type size x86 x64
===== ====== ==== === ===
a int 4 4 8
b byte 1 14 18
c int 4 8 12
d String 4/8 0 0
e short 2 12 16

La cosa più importante da notare in questa tabella è che, (come menzionato da Hans ), gli offset di campo segnalati non sono monotoni rispetto al loro ordine di dichiarazione. I campi delle istanze ValueType vengono sempre riordinati in modo che tutti i campi tipizzati di riferimento vadano per primi. Possiamo vedere che il campo String d è all’offset 0.

Un ulteriore riordino ottimizza l’ordine dei campi al fine di condividere l’eccesso di padding interno che altrimenti verrebbe sprecato. Possiamo vederlo con il campo byte b , che è stato spostato dall’essere il secondo campo dichiarato, fino all’essere l’ultimo.

Naturalmente, ordinando le righe della tabella precedente possiamo rivelare il vero layout interno gestito di un valore .NET . Si noti che siamo in grado di ottenere questo layout nonostante la struct T esempio che contiene un riferimento gestito ( String d ) e che quindi non sia blittabile :

============= x86 ============ ============= x64 ============
field type size offs end field type size offs end field type size offs end field type size offs end
===== ====== ==== ==== === ===== ====== ==== ==== ===
d String 4 0 … 4 d String 8 0 … 8
a int 4 4 … 8 a int 4 8 … 12
c int 4 8 … 12 c int 4 12 … 16
e short 2 12 … 14 e short 2 16 … 18
b byte 1 14 … 15 b byte 1 18 … 19

internal padding: 1 15 … 16 internal padding: 5 19 … 24

x86 managed total size: 16 x64 managed total size: 24

In precedenza abbiamo determinato la dimensione di una singola istanza di struct gestita calcolando la differenza di offset di byte tra istanze adiacenti. Tenendo conto di ciò, le ultime righe della tabella precedente mostrano banalmente il riempimento che il CLR applica internamente alla fine della struttura di esempio T Ricorda di nuovo, naturalmente, che questa imbottitura interna è fissata dal CLR e completamente fuori dal nostro controllo.

5. Coda
Per completezza, questa ultima tabella mostra la quantità di riempimento che verrà sintetizzata al volo durante il marshalling . Si noti che in alcuni casi, questo padding Marshal è negativo rispetto alla dimensione interna gestita. Ad esempio, anche se la dimensione interna gestita di una T in x64 è di 24 byte, la struct emessa dal marshalling può essere di 19 o 20 byte con Pack=1 o Pack=2 , rispettivamente.

pack size offs end pack size offs end
============= ==== ==== === ============= ==== ==== ===
1 0 15 … 15 1 0 19 … 19
2 1 15 … 16 2 1 19 … 20
4/8/16/32/64… 5 15 … 20 4/8/16/32/64… 5 19 … 24