Claudio Brotto

Struct e Padding

Ho letto con interesse questo post di Andrea Boschin relativo alle dimensioni in memoria delle struct.
Trovo sicuramente valide le considerazioni di Andrea: un risparmio di 4 byte è nulla sul singolo elemento, ma può assumere proporzioni ben maggiori su collection ben .. nutrite.
Ciò che, a mio parere, è ancora più importante è che una scarsa attenzione al layout di una struct può determinare effetti ben peggiori che alcuni mega di memoria virtuale occupata.
Mi riferisco alle situazioni di interoperabilità con applicazioni non gestite.

Immaginate di dover accedere ad un'area di memoria non gestita tramite la seguente struttura:

struct MyStruct {
  UInt16 wordValue;
  UInt32 dwordValue;
}

La classe System.Runtime.InteropServices.Marshal offre diversi metodi statici che consentono l'interazione con un ambiente non gestito.
Tra questi ci viene in aiuto, nel nostro esempio, il metodo PtrToStructure, che accetta un IntPtr e un'istanza di Type (il tipo che vogliamo marshalare) e ritorna un Object "castabile" al nostro oggetto target.
Per copiare i dati dall'area non gestita a quella gestita, la classe Marshal va a controllare le dimensioni, in byte, del tipo destinazione, e tale numero di byte viene copiato all'interno dello heap gestito.

Nel nostro caso, quanti byte occupa la struttura MyStruct ?
6, senza ombra di dubbio ! Peccato che, per ragioni di padding (rimando nuovamente al post di Andrea e all'articolo su MSDN che Andrea linka), la struttura occupa 8 byte.
Inutile dire che le conseguenze non sono esattamente piacevoli.

Il primo consiglio, in situazioni del genere, è di esplicitare il layout di una struct tramite l'attributo System.Runtime.InteropServices.StructLayoutAttribute, specificando il tipo di layout esplicito.
In tal modo, la definizione di MyStruct diventa:

[StructLayout (LayoutKind.Explicit)]
struct MyStruct {
  UInt16 wordValue;
  UInt32 dwordValue;
}

Questo codice non compila, non ancora, per lo meno. Infatti, quando viene richiesto un layout esplicito per un tipo, occorre indicare al runtime gli offset dei singoli campi.
Niente di più facile.
Riscriviamo MyStruct:

[StructLayout (LayoutKind.Explicit)]
struct MyStruct {
  [FieldOffset (0)]
  UInt16 wordValue;
  [FieldOffset (2)]
  UInt32 dwordValue;
}

In questo modo, il campo dwordValue viene letto dalla posizione di memoria immediatamente contigua a wordValue, senza che vengano inseriti i 2 byte di padding.

Quindi, finalmente, MyStruct occupa .... 8 byte !

Ma come ?!?
Beh, in effetti non abbiamo fatto altro che "spostare" i due byte di padding in fondo alla struttura !
Se controlliamo le dimensioni di MyStruct (tramite Marshal.SizeOf (typeof (MyStruct))) il valore che ci viene ritornato è 8.

In un errore di questo tipo sono incappato alcuni mesi fa: dovevo accedere ad un MemoryMappedFile da un'applicazione .NET. Avevo a disposizione una classe a cui dovevo passare semplicemente l'offset di un dato a partire dal suo IntPtr di base.
La mia struttura (era un po' più lunga di MyStruct, ovviamente, ma poco cambia) affermava di essere grossa 124 byte (se non ricordo male), mentre in realtà i byte erano 123.
Dovendo accedere ad un array di strutture, non facevo altro che richiamare la funzione di accesso passando un valore intero, incrementato all'interno di un ciclo di Marshal.SizeOf(typeof (MyOtherStruct)) byte, cioè 124.
Verso la fine del ciclo, immancabile giungeva un'eccezione Win32: stavo leggendo dove non dovevo !

E allora ?

La soluzione è stata semplice: l'attributo StructLayout accetta un parametro Size, tramite il quale si può specificare la dimensione reale della struttura.
Impostato quello, tutto è filato liscio

... beh, sino al bug successivo, ma questa è un'altra storia !

 

powered by IMHO

Posted: gen 04 2005, 10:05 by devlizard
Filed under: