Sappiamo tutti che CPU e memoria sono le due metriche più importanti per un programma, quindi quante persone hanno davvero riflettuto sulla domanda: quanti byte occupa un'istanza di un tipo (tipo valore o tipo di riferimento) in memoria? Molti di noi non possono rispondere. C# fornisce alcuni operatori e API per calcolare le dimensioni, ma nessuno di questi risolve completamente il problema che ho appena chiesto. Questo articolo fornisce un metodo per calcolare il numero di byte di memoria occupati da istanze di tipi di valore e tipi di riferimento. Il codice sorgente viene scaricato da qui.
1. dimensione dell'operatore 2. Metodo Marshal. DimensioneDi 3. Non sicuro. Dimensione del metodo > 4. Si può calcolare in base al tipo di membro del campo? 5. Disposizione dei tipi di valore e dei tipi di applicazione 6. Direttiva LDFLDA 7. Calcolare il numero di byte del tipo di valore 8. Contare il numero di byte del tipo di citazione 9. Calcolo completo
1. dimensione dell'operatore
L'operazione dimensionof viene utilizzata per determinare il numero di byte occupati da un'istanza di un tipo, ma può essere applicata solo ai tipi non gestiti. Il cosiddetto tipo Non gestito è limitato a:
Tipi primitivi: booleano, byte, SByte, Int16, UInt16, Int32, UInt32, Int64, UInt64, IntPtr, UIntPtr, Char, Double e Single) Tipo decimale Tipo di enumerazione Tipo di puntatore Struct che contengono solo membri dati di tipo Non gestito Come suggerisce il nome, un tipo Non gestito è un tipo di valore, e l'istanza corrispondente non può contenere alcun riferimento all'oggetto gestito. Se definiamo un metodo generico come questo per chiamare l'operatore sizeof, il parametro generico T deve aggiungere un vincolo non gestito e un tag non sicuro al metodo.
Solo i tipi nativi ed enum possono usare direttamente l'operatore sizeof, che deve essere aggiunto se applicato ad altri tipi (puntatori e struct personalizzate)./unsafee devono essere inseriti inpericolosonel contesto.
Poiché la seguente struct Foobar non è di tipo Non gestito, il programma avrà un errore di compilazione.
2. Metodo Marshal. DimensioneDi
Tipi statici Marshal definisce una serie di API che ci aiutano ad allocare e copiare memoria non gestita, convertire tra tipi gestiti e non gestiti, ed eseguire una serie di altre operazioni sulla memoria non gestita (Marshal nella scienza computazionale si riferisce all'operazione di conversione degli oggetti di memoria nel corrispondente formato per l'archiviazione o il trasferimento dei dati). Statico, che include i seguenti 4 sovraccarichi di metodo SizeOf per determinare il numero di byte di un dato tipo o oggetto.
Il metodo Marshal.SizeOf non ha una restrizione sul tipo specificato per il tipo Unmanaged, ma richiede comunque che una venga specificataTipo di valore。 Se l'oggetto in arrivo è un oggetto, deve essere anche una casella per un tipo di valore.
Poiché il seguente Foobar è definito come:gentile, quindi le chiamate a entrambi i metodi SizeOf lancieranno un'eccezione ArgumentException e un prompt: Il tipo 'Foobar' non può essere marshalizzato come struttura non gestita; non è possibile calcolare alcuna dimensione o offset significativo.
Metodo Marshal. DimensioneOfI generici non sono supportati, ma ha anche requisiti per la disposizione della struttura, che supporta il supportoSequenzialeeEsplicitoModalità layout. Poiché la struttura Foobar mostrata qui sotto adotta la modalità Auto layout (Auto, che non supporta la "pianificazione dinamica" della disposizione della memoria basata sui membri del campo a causa dei requisiti più rigorosi di layout della memoria negli ambienti non gestiti), le chiamate al metodo SizeOf mostreranno comunque la stessa eccezione ArgumentException di sopra.
3. Metodo Insicuro. DimensioneDel metodo
Static Unsafe fornisce operazioni di basso livello più basse per la memoria non gestita, e metodi simili di SizeIOf sono definiti in questo tipo. Il metodo non ha restrizioni sul tipo specificato, ma se specifichi un tipo di riferimento, restituisce ilNumero di byte del puntatore"(IntPtr.Size)。
4. Si può calcolare in base al tipo di membro del campo?
Sappiamo che sia i tipi di valore che quelli di riferimento sono mappati come frammenti continui (o memorizzati direttamente in un registro). Lo scopo di un tipo è specificare la disposizione della memoria di un oggetto, e le istanze dello stesso tipo hanno la stessa disposizione e il numero di byte è naturalmente lo stesso (per i campi di tipo di riferimento, memorizza solo l'indirizzo di riferimento in questa sequenza di byte). Poiché la lunghezza del byte è determinata dal tipo, se riuscissimo a determinare il tipo di ogni membro del campo, non potremmo calcolare il numero di byte corrispondenti a quel tipo? In realtà, non è possibile.
Ad esempio, sappiamo che i byte di byte, short, int e long sono 1, 2, 4 e 8, quindi il numero di byte per un binario di byte è 2, ma per una combinazione di tipi byte + short, byte + int e byte + long, i byte corrispondenti non sono 3, 5 e 9, ma 3, 8 e 16. Perché questo comporta la questione dell'allineamento della memoria.
5. Disposizione dei tipi di valore e dei tipi di riferimento
Il numero di byte occupati dalle istanze del tipo e sottotipo di riferimento è diverso anche per lo stesso identico membro dei dati. Come mostrato nell'immagine seguente, la sequenza di byte dell'istanza di tipo di valoreTutti sono membri di campo usati per conservarla。 Per le istanze dei tipi di riferimento, l'indirizzo della tabella di metodo corrispondente al tipo viene anche memorizzato davanti alla sequenza di byte del campo. La tabella dei metodi fornisce quasi tutti i metadati che descrivono il tipo, e usiamo questo riferimento per determinare a quale tipo appartiene l'istanza. All'inizio ci sono anche byte extra, che chiameremoIntestazione dell'oggettoNon viene usato solo per memorizzare lo stato bloccato dell'oggetto, ma il valore hash può anche essere memorizzato nella cache qui. Quando creiamo una variabile di tipo di riferimento, questa variabileNon punta al primo byte di memoria occupato dall'istanza, ma al punto in cui è memorizzato l'indirizzo della tabella dei metodi。
6. Direttiva LDFLDA
Come abbiamo introdotto sopra, l'operatore sizeof e il metodo SizeOf fornito dal tipo statico Marshal/Unsafe non possono davvero risolvere il calcolo della lunghezza del byte occupato dalle istanze. Per quanto ne so, questo problema non può essere risolto solo nel campo C#, ma viene fornito a livello di ILLdfldaLe istruzioni possono aiutarci a risolvere questo problema. Come suggerisce il nome, Ldflda sta per Load Field Address, che ci aiuta a ottenere l'indirizzo di un campo nell'istanza. Poiché questa istruzione IL non ha un'API corrispondente in C#, possiamo usarla solo nella seguente forma usando IL Emit.
Come mostrato nel frammento di codice sopra, abbiamo un metodo GenerateFieldAddressAccessor nel tipo SizeCalculator, che genera un delegato di tipo Func<object?, long[]> basato sulla lista dei campi del tipo specificato, che ci aiuta a restituire l'indirizzo di memoria dell'oggetto specificato e di tutti i suoi campi. Con l'indirizzo dell'oggetto stesso e l'indirizzo di ogni campo, possiamo naturalmente ottenere lo spostamento di ogni campo e poi calcolare facilmente il numero di byte di memoria occupati dall'intera istanza.
7. Calcolare il numero di byte del tipo di valore
Poiché i tipi di valore e i tipi di riferimento hanno disposizioni diverse in memoria, dobbiamo anche utilizzare calcoli differenti. Poiché il byte della struct è il contenuto di tutti i campi in memoria, usiamo un modo ingegnoso per calcolarlo. Supponiamo di dover stabilizzare il numero di byte di una struct di tipo T, creiamo una tupla ValueTuple<T,T> e l'offset del suo secondo campo Item2 è il numero di byte della struct T. Il metodo di calcolo specifico è riflesso nel seguente metodo CalculateValueTypeInstance.
Come mostrato nel frammento di codice sopra, assumendo che il tipo di struct da calcolare sia T, chiamiamo il metodo GetDefaultAsObject per ottenere l'oggetto default(T) sotto forma di riflessione, e poi creiamo un ValueTuple<T,T>tuple. Dopo aver chiamato il metodo GenerateFieldAddressAccessor per ottenere il delegato Func<object?, long[]> per calcolare l'istanza e i suoi indirizzi di campo, chiamiamo questo delegato come argomento. Per i tre indirizzi di memoria che otteniamo, la tupla di codice e gli indirizzi dei campi 1 e 2 sono gli stessi, usiamo il terzo indirizzo che rappresenta Item2 meno il primo indirizzo, e otteniamo il risultato desiderato.
8. Contare il numero di byte del tipo di citazione
Il calcolo di byte per i tipi di riferimento è più complicato, usando questa idea: dopo aver ottenuto l'indirizzo dell'istanza stessa e di ogni campo, ordiniamo gli indirizzi per ottenere lo offset dell'ultimo campo. Aggiungiamo questo offset al numero di byte dell'ultimo campo stesso, e poi aggiungiamo il "primo e ultimo byte" necessari al risultato desiderato, che si riflette nel seguente metodo CalculateReferneceTypeInstance.
Come mostrato nel frammento di codice sopra, se il tipo specificato non ha campi definiti, CalculateReferneceTypeInstance restituisce il numero minimo di byte dell'istanza del tipo di riferimento: 3 volte il numero di byte del puntatore di indirizzo. Per le architetture x86, un oggetto di tipo applicazione occupa almeno 12 byte, inclusi ObjectHeader (4 byte), puntatori della tabella dei metodi (byte) e almeno 4 byte di contenuto del campo (questi 4 byte sono necessari anche se nessun tipo è definito senza campi). Nel caso dell'architettura x64, questo numero minimo di byte sarà 24, perché il puntatore della tabella dei metodi e il contenuto minimo del campo diventeranno 8 byte, anche se il contenuto valido dell'ObjectHeader occupa solo 4 byte, ma 4 byte di padding verranno aggiunti all'inizio.
La distribuzione dei byte occupati dall'ultimo campo è anch'essa molto semplice: se il tipo è un tipo di valore, allora viene chiamato il metodo CalculateValueTypeInstance definito in precedenza per calcolare; se è un tipo di riferimento, il contenuto memorizzato nel campo è solo l'indirizzo di memoria dell'oggetto di destinazione, quindi la lunghezza è IntPtr.Size. Poiché le istanze di tipo di riferimento sono allineate di default con IntPtr.Size in memoria, questo avviene anche qui. Infine, non dimenticare che il riferimento dell'istanza di tipo di riferimento non punta al primo byte di memoria, ma al byte che memorizza il puntatore della tabella dei metodi, quindi devi aggiungere il numero di byte di ObjecthHeader (IntPtr.Size).
9. Calcolo completo
I due metodi utilizzati per calcolare il numero di byte di istanze di tipo valore e tipo di riferimento sono utilizzati nel seguente metodo SizeOf. Poiché la chiamata dell'istruzione Ldflda deve fornire un'istanza corrispondente, questo metodo fornisce un delegato per ottenere l'istanza corrispondente oltre a fornire il tipo di destinazione. I parametri corrispondenti a questo delegato possono essere predefiniti e useremo il valore predefinito per il tipo di valore. Per i tipi di riferimento, cercheremo anche di creare l'oggetto target usando il costruttore predefinito. Se questo oggetto delegato non viene fornito e l'istanza di destinazione non può essere creata, il metodo SizeOf lancia un'eccezione. Anche se dobbiamo fornire l'istanza target, il risultato calcolato è solo correlato al tipo, quindi memorizziamo in cache il risultato calcolato. Per facilità di chiamata, forniamo anche un <T>altro metodo generico SizeOf.
Nel codice snippet qui sotto, lo usiamo per fornire il numero di byte di due struct e tipi con la stessa definizione di campo. Nel prossimo articolo otterremmo ulteriormente il contenuto binario completo dell'istanza in memoria basato sul numero calcolato di byte, quindi restate sintonizzati.
Link originale:Il login del link ipertestuale è visibile. |