| Pierre Greborio |
Dimensione degli oggetti managed
Ciao,
questo messaggio nasce da una discussione molto interessante svoltasi ieri al lancio di Visual Sutio .NET 2003 con Raffaele Rialdi. La domanda era: "E' possibile ottenere la domensione di un oggetto managed ?". Ahinoi, la gestione della memoria del framework è un argomento un pò oscuro in quanto, pur essendo ben documentato (vedasi, ad esempio il testo di Richter), è molto difficile averne la prova (con certezza). Per l'occasione mi sono riferito ad un messaggio di Chris Brumme (Software Architect in Microsoft) disponibile a http://blogs.gotdotnet.com/cbrumme/PermaLink.aspx/6191a0fa-0c5d-4eb9-b3bb-d567996e6fc9 che dice: "We don't expose the managed size of objects because we want to reserve the ability to change the way we lay these things out. For example, on some systems we might align and pack differently. For this to happen, you need to specify tdAutoLayout for the layout mask of your ValueType or Class. If you specify tdExplicitLayout or tdSequentialLayout, the CLR’s freedom to optimize your layout is constrained. If you are curious to know how big an object happens to be, there are a variety of ways to discover this. You can look in the debugger. For example, Strike or SOS (son-of-strike) shows you how objects are laid out. Or you could allocate two objects and then use unverifiable operations to subtract the addresses. 99.9% of the time, the two objects will be adjacent. You can also use a managed profiler to get a sense of how much memory is consumed by instances of a particular type. But we don't want to provide an API, because then you could form a dependency over this implementation detail. Some people have confused the System.Runtime.InteropServices.Marshal.SizeOf() service with this API. However, Marshal.SizeOf reveals the size of an object after it has been marshaled. In other words, it yields the size of the object when converted to an unmanaged representation. These sizes will certainly differ if the CLR’s loader has re-ordered small fields so they can be packed together on a tdAutoLayout type." La domanda che mi è quindi sorta è, ma a che tipo di oggetto si riferisce ? Reference type o Value Type o entrambi ? La risposta, parziale la si trova in un'altro weblog di Sam Gentile (http://dotnetweblogs.com/sgentile/archive/04152003.aspx). Da quest'ultimo log si capisce che il precendente messaggio (di Chris) si limita ai reference types. Ma c'è ancora una cosa poco chiara, che succede con il tipo char ? Nel libro di Adam Nathan (un riferimento per l'interoperability) si diche che il tipo char è un pò "difficile" quando portato nel mondo Unmanaged in quanto dipende dal tipo di codifica che gli viene assegnato (charset). Per default il tipo char (System.Char) in .NET è unicode e quando è trasposto nel mondo unmagaed per default è ANSI. Quindi, se si vuole uniformità bisognerebbe marcare la struttura con un charset unicode (esempio preso dal blog di Sam): [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] public struct HowBigAmi { public char one; public char two; public char three; } In conclusione, se è vero che è possibile determinare la dimensione di un Value Type (con certe accortezze), non è altrettanto vero per una classe. Ciao Pierre |
| Raffaele Rialdi |
Re: Dimensione degli oggetti managed
[Post lungo e puramente accademico]
Ciao Pierre, rieccomi sull'argomento. Spero che altri vorranno unirsi a noi in questa discussione. Per cercare di capire meglio cosa succede in territorio managed, ho dato un occhiata ai preziosi sorgenti del cli (rotor). Quanto vado a scrivere è una interpretazione di quello che leggo nei sorgenti, potrei prendere cantonate e nel caso vorrei leggere le opinioni di tutti. I reference types (o i value types boxed) sono a tutti gli effetti delle classi c++ (sempre restando in ambiente managed) chiamate EEClass. Questa classe rappresenta un generico reference type di tipo managed. Mentre in c++ siamo abituati a considerare la dimensione di una classe (sizeof()) come i dati di una certa istanza (quindi la dimensione del this) in ambiente managed il discorso è diverso perchè la classe c++ a cui fa riferimento la classe managed è un garbuglio di puntatori a tutto ciò che vediamo in un reference type in c# o vb.net. Mi spiego meglio. Se in una classe managed abbiamo tre metodi e un intero membro della classe, la classe c++ del cli che la rappresenta non contiene i tre metodi e l'intero ma un enorme insieme di puntatori, enumerazioni, tabelle che permettono al mondo managed di gestire metodi e membri anche in modo dinamico, al runtime. Altrimenti non funzionerebbe la reflection. Mi sono fatto quest'idea dando un occhio ai sorgenti class.cpp e class.h. EEClass rappresenta la generica classe managed, e LayoutEEClass una classe a cui è stato impostato un layout manuale (via attributo). In questa enorme classe trovate un metodo interessante: EEClass::SetupMethodTable che organizza la method table del reference type. Poco più avanti, in questo metodo si trovano queste interessanti righe di codice: if (!IsInterface()) { m_pMethodTable->m_BaseSize = (DWORD) MAX(m_dwNumInstanceFieldBytes + ObjSizeOf(Object), MIN_OBJECT_SIZE); m_pMethodTable->m_BaseSize = (m_pMethodTable->m_BaseSize + ALLOC_ALIGN_CONSTANT) & ~ALLOC_ALIGN_CONSTANT; // m_BaseSize must be dword aligned m_pMethodTable->SetComponentSize(0); } Considerazione 1. Un oggetto non potrà mai sessere più piccolo di MIN_OBJECT_SIZE Considerazione 2. La dimensione di base che stiamo cercando è ObjSizeOf(Object) a cui vanno aggiunti alcuni byte per l'allineamento (nel caso sia gestito automaticamente, cioè senza attributo di Layout) Considerazione 3. La classe c++ occupa molta più memoria perchè si tira dietro tutte quelle cose di cui parlavo prima, quantificate in quelle righe da m_dwNumInstanceFieldBytes. La definizione di ObjSizeOf si trova nel sorgente syncblk.h (che è il gestore della sincronia nel multithreading): // Every Object is preceded by an ObjHeader (at a negative offset). The // ObjHeader has an index to a SyncBlock. This index is 0 for the bulk of all // instances, which indicates that the object shares a dummy SyncBlock with // most other objects. // ... e poco più avanti dichiara: // The true size of an object is whatever C++ thinks, plus the ObjHeader we // allocate before it. #define ObjSizeOf(c) (sizeof(c) + sizeof(ObjHeader)) La frase: "whatever c++ thinks" deriva dal fatto che in C una struct di 3 byte e una dword occupa 8 byte per motivi di allineamento. Quindi la dimensione di base del reference type è la dimensione c++ più l'header del sync. A questo punto abbiamo chiarito alcuni piccoli punti: 1. In managed, avere la dimensione esatta dell'oggetto non serve perchè non posso toccarlo direttamente. 2. In unmanaged Marshal.SizeOf(), di cui anche in rotor sono assenti i sorgenti, porta in unamanged la struct e poi ne calcola la dimensione con sizeof() del c++. 3. La gestione del reference type è abissalmente diversa da quella del value type (da quanto ho potuto vedere nei sorgenti), e questo giustifica la grossa perdita di performance nelle operazioni di box/unbox. 4. Per un referece type, se anche potessimo avere la sizeof() dell'oggetto managed, la vorremmo per i soli dati membri della classe o compresa di tutti gli ammenicoli necessari al mondo managed? Quest'ultimo punto mi ha fatto cambiare idea sulla necessità di avere la vera dimensione dell'oggetto managed. La magra soddisfazione è quella di vedere dai sorgenti che effettivamente, in caso di [StructLayout(LayoutKind.Sequential)], il layout viene rispettato anche in managed per cui la dimensione di base della struct è proprio la stessa del c++ unamanged. Commenti? Critiche? Non sparate sul pianista. Molte sono mie deduzioni anche se sulla base di alcuni punti fissi visti nei sorgenti. Raffaele |
| Pierre Greborio |
Re: Dimensione degli oggetti managed
Ciao Raffaele,
> rieccomi sull'argomento. Spero che altri vorranno unirsi a noi in questa > discussione. Concordo. > Per cercare di capire meglio cosa succede in territorio managed, ho dato un occhiata > ai preziosi sorgenti del cli (rotor). > Quanto vado a scrivere è una interpretazione di quello che leggo nei sorgenti, > potrei prendere cantonate e nel caso vorrei leggere le opinioni di tutti. Potrebbe essere una cantonata come non esserlo. Dipende da quanto è allineato Rotor al CLR che usamo. Penso che essendo la gestione della memoria la più "toccata" dal team (in termini migliorativi ovviamente) non mi stupirei se ci fossero delle discrepanze. > I reference types (o i value types boxed) sono a tutti gli effetti delle classi c++ > (sempre restando in ambiente managed) chiamate EEClass. > Questa classe rappresenta un generico reference type di tipo managed. Non vorrei fare una assunzione errata, ma credo che l'EEClass sia letta all'inizio (loader managed) in quanto contiene il layout della classe. Se ricordo bene, quanto parte il runtime la prima cosa che fa è caricarsi tutte le EEClasses come se riflettesse il metatada. > Considerazione 1. Un oggetto non potrà mai sessere più piccolo di MIN_OBJECT_SIZE Interessante. Sarà mira la dimensione di System::Object ? > 1. In managed, avere la dimensione esatta dell'oggetto non serve perchè non posso > toccarlo direttamente. Che succede se vuoi implementarti un custom marshaler ? Non verrebbe comodo poterlo toccare ? > 2. In unmanaged Marshal.SizeOf(), di cui anche in rotor sono assenti i sorgenti, > porta in unamanged la struct e poi ne calcola la dimensione con sizeof() del c++. Confermo. > 3. La gestione del reference type è abissalmente diversa da quella del value type > (da quanto ho potuto vedere nei sorgenti), e questo giustifica la grossa perdita di > performance nelle operazioni di box/unbox. Confermo. > 4. Per un referece type, se anche potessimo avere la sizeof() dell'oggetto managed, > la vorremmo per i soli dati membri della classe o compresa di tutti gli ammenicoli > necessari al mondo managed? Averlo sui soli dati membri allora è meglio usare una value type (struct). In ogni caso non sarebbe male averlo. > Commenti? Critiche? Ottima analisi. Ciao Pierre |
| Raffaele Rialdi |
Re: Dimensione degli oggetti managed
> Potrebbe essere una cantonata come non esserlo. Dipende da quanto è allineato Rotor al
> CLR che usamo. Penso che essendo la gestione della memoria la più "toccata" dal team (in > termini migliorativi ovviamente) non mi stupirei se ci fossero delle discrepanze. Si, potrebbero esserci delle discrepanze ma in linea di massima l'implementazione è quella. In ogni caso i risultati si possono controllare sotto debugger. > Non vorrei fare una assunzione errata, ma credo che l'EEClass sia letta all'inizio > (loader managed) in quanto contiene il layout della classe. Se ricordo bene, quanto parte > il runtime la prima cosa che fa è caricarsi tutte le EEClasses come se riflettesse il > metatada. Siamo allineati. Io ho potuto vedere che calcola la dimensione degli oggetti a seconda del tipo di layout, sia esso automatico o imposto dall'utente. > > Considerazione 1. Un oggetto non potrà mai sessere più piccolo di MIN_OBJECT_SIZE > Interessante. Sarà mira la dimensione di System::Object ? È definito come: #define MIN_OBJECT_SIZE (2*sizeof(BYTE*) + sizeof(ObjHeader)) dove ObjHeader è la classe che precede i reference type che ne permettono la sincronizzazione (infatti lock è uno statement valido solo per i reference type). ObjHeader credo di aver capito che contenga solo una DWORD, quindi MIN_OBJECT_SIZE sarebbe 10 byte. > > 2. In unmanaged Marshal.SizeOf(), di cui anche in rotor sono assenti i sorgenti, > > porta in unamanged la struct e poi ne calcola la dimensione con sizeof() del c++. > Confermo. Sul newsgroup del sscli, ho chiesto circa i sorgenti del SizeOf e mi hanno imbeccato subito sul file comndirect.cpp. Da quello che ho visto, la verità potrebbe essere in mezzo e potrebbe dare un interpretazione molto diversa al blog di Chris Brumme. Guarda il sorgente comndirect.cpp e cerca FCSizeOfObject e SizeOfClass. Questi sono proprio le implementazioni di Marshal.SizeOf. Il primo per i value types, il secondo per i reference types. Enterambi i metodi non eseguono alcuna copia dei dati ma semplicemente vanno a leggere la MethodTable (file class.cpp) e ritornare m_cbNativeSize. Tutto ciò potrebbe dire che la dimensione base degli oggetti Managed è esattamente la stessa di quella unmanaged ma poi ci sono tutti i fronzoli di cui parlavo nel mio precedente post. I fronzoli sarebbero proprio quelli che Brumme non voleva esporre ai programmi managed e qui mi posso trovare daccordo. Che ne dici? > Che succede se vuoi implementarti un custom marshaler ? Non verrebbe comodo poterlo > toccare ? Questo punto l'ho spostato perchè se è vero quello che ho appena scritto, i fronzoli sarebbero proprio tutte quelle informazioni inutili al momento del marshaling. Client e server devono necessariamente conoscere i tipi che stanno spostando. Di conseguenza conoscono anche i fronzoli (mamma mia che termini!), perchè questi sono tutte le informazioni di reflection (metadata), layout, etc. che il 'type' del framework descrive. Se così fosse, non servirebber al custom masrheler, non trovi? Ottimi punti anche a te Pierre. Raffaele |
| Pierre Greborio |
Re: Dimensione degli oggetti managed
> Enterambi i metodi non eseguono alcuna copia dei dati ma semplicemente vanno a leggere la
> MethodTable (file class.cpp) e ritornare m_cbNativeSize. Esatto, ma dove è inizializzato m_cbNativeSize ? Seguire i "giri" che fa con il solo sorgente SSCLI è abbastanza arduo, ma di fatto sembra che questa sia la chiave di tutto. > Tutto ciò potrebbe dire che la dimensione base degli oggetti Managed è esattamente la stessa > di quella unmanaged ma poi ci sono tutti i fronzoli di cui parlavo nel mio precedente post. > I fronzoli sarebbero proprio quelli che Brumme non voleva esporre ai programmi managed e qui > mi posso trovare daccordo. Che ne dici? Che via sia una parte in comune direi di si, ma quanto ? > Questo punto l'ho spostato perchè se è vero quello che ho appena scritto, i fronzoli sarebbero > proprio tutte quelle informazioni inutili al momento del marshaling. Client e server devono > necessariamente conoscere i tipi che stanno spostando. Esatto. Vorrei approfondire meglio alcuni aspetti dei custom marshaler :-) > Di conseguenza conoscono anche i > fronzoli (mamma mia che termini!), perchè questi sono tutte le informazioni di reflection > (metadata), layout, etc. che il 'type' del framework descrive. > Se così fosse, non servirebber al custom masrheler, non trovi? Penso al custom marshaler come ad un semplice mapper, nulla più. Ma per scrivere un mapper bisogna assolutamente conoscere i due end-point. Adrò a recuperare il libro di Adams. Ciao Pierre |