Estoy tratando de entender por qué una estructura que contiene solo int toma 8 bytes de memoria dentro de una clase.
considerando el siguiente código;
static void Main() { var rand = new Random(); var twoIntStruct = new TwoStruct(new IntStruct(rand.Next()), new IntStruct(rand.Next())); var twoInt = new TwoInt(rand.Next(), rand.Next()); Console.ReadLine(); } public readonly struct IntStruct { public int Value { get; } internal IntStruct(int value) { Value = value; } } public class TwoStruct { private readonly IntStruct A; private readonly IntStruct B; public TwoStruct( IntStruct a, IntStruct b) { A = a; B = b; } } public class TwoInt { private readonly int A; private readonly int B; public TwoInt( int a, int b) { A = a; B = b; } }ahora, cuando perfilo estas dos instancias con dotMemory, obtengo el siguiente resultado:
Aunque tanto int como intStruct ocupan 4 bytes de memoria en la pila, parece que el tamaño de la clase en el montón es diferente y esa estructura siempre está alineada con 8 bytes.
¿Qué puede causar este comportamiento?
Para las clases, el diseño de memoria predeterminado es "Auto", lo que significa que CLR decide cómo alinear los campos en una clase en la memoria. Es un detalle de implementación no documentado. Por algún motivo que desconozco, alinea los campos de los tipos de valores personalizados en un límite de tamaño de puntero (es decir, 8 bytes en un proceso de 64 bits, 4 bytes en un proceso de 32 bits).
Si compila ese código en 32 bits, verá que tanto TwoInt como TwoStruct ahora toman 16 bytes (4 para el encabezado del objeto, 4 para el puntero de la tabla de métodos y luego 8 para los campos), porque ahora están alineados en 4 bytes. Perímetro.
En el caso de 64 bits, como en su pregunta, los tipos de valores personalizados están alineados en un límite de 8 bytes, por lo que TwoStruct tiene un diseño de:
Object Header (8 bytes) Method Table Pointer (8 bytes) IntStruct A (4 bytes) padding (4 bytes, to align at 8 bytes) IntStruct B (4 bytes) padding (4 bytes) Y TwoInt es solo
Object Header (8 bytes) Method Table Pointer (8 bytes) IntStruct A (4 bytes) IntStruct B (4 bytes) Debido a que int no es un tipo de valor personalizado, CLR no lo alinea en el límite del tamaño del puntero. Si en lugar de IntStruct LongStruct y long en lugar de int , entonces ambos casos tendrían el mismo tamaño, porque long es de 8 bytes e incluso para la estructura personalizada, CLR no necesitará agregar ningún relleno para alinearlo en el límite de 8 bytes en 64 -poco.
Aquí hay un artículo interesante relacionado con el tema. El autor desarrolla una herramienta bastante interesante para inspeccionar el diseño de la memoria de los objetos directamente desde el código .NET (sin herramientas externas). Él investiga este mismo problema y llega a la conclusión anterior:
Si el diseño de tipo es LayoutKind.Auto, CLR rellenará cada campo de un tipo de valor personalizado. Esto significa que si tiene varias estructuras que envuelven solo un int o byte y se usan ampliamente en millones de objetos, ¡podría tener una sobrecarga de memoria notable debido al relleno!
Puede afectar el diseño administrado de una clase con StructLayouAttribute con LayoutKind = Sequential SI todos los campos en esta clase son blittables (que es el caso en esta pregunta):
Para los tipos blittables, LayoutKind.Sequential controla tanto el diseño en la memoria administrada como el diseño en la memoria no administrada. Para los tipos que no se pueden dividir, controla el diseño cuando la clase o la estructura se calculan en código no administrado, pero no controla el diseño en la memoria administrada.
Entonces, como se menciona en los comentarios, podemos eliminar el relleno haciendo:
[StructLayoutAttribute(LayoutKind.Sequential, Pack = 4)] public class TwoStruct { private readonly IntStruct A; private readonly IntStruct B; public TwoStruct( IntStruct a, IntStruct b) { A = a; B = b; } }Lo que en realidad nos ahorrará algo de memoria.