For classes, default memory layout is "Auto", which means CLR decides itself how to align fields in a class in memory. It's an undocumented implementation detail. For some reason unknown to me, it aligns fields of custom value types at a pointer size boundary (so, 8 bytes in 64-bit process, 4 bytes in 32-bit).
If you compile that code in 32-bit, you will see that both TwoInt
and TwoStruct
now take 16 bytes (4 for object header, 4 for method table pointer, and then 8 for fields), because now they are aligned at 4-byte boundary.
At 64-bit case, like in your question, custom value types are aligned at 8-byte boundary, so TwoStruct
has layout of:
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)
And TwoInt
is just
Object Header (8 bytes)
Method Table Pointer (8 bytes)
IntStruct A (4 bytes)
IntStruct B (4 bytes)
Becauseint
is not a custom value type - CLR does not align it at pointer size boundary. If instead of IntStruct
we used LongStruct
and long
instead of int
- then both cases would have the same size, because long
is 8 bytes and even for custom struct CLR will not need to add any padding to align it at 8-byte boundary in 64-bit.
Here is an interesting article related to the issue. The author develops pretty interesting tool to inspect memory layout of objects directly from .NET code (without external tools). He investigates this same issue and cames to the conclusion above:
If the type layout is LayoutKind.Auto the CLR will pad each field of a
custom value type! This means that if you have multiple structs that
wrap just a single int or byte and they’re widely used in millions of
objects, you could have a noticeable memory overhead due to padding!
You can affect the managed layout of a class with StructLayouAttribute
with LayoutKind = Sequential
IF all fields in this class are blittable (which is the case in this question):
For blittable types, LayoutKind.Sequential controls both the layout in
managed memory and the layout in unmanaged memory. For non-blittable
types, it controls the layout when the class or structure is marshaled
to unmanaged code, but does not control the layout in managed memory
So as mentioned in comments, we can remove the padding by doing:
[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;
}
}
Which will actually save us some memory.
[StructLayoutAttribute(LayoutKind.Sequential, Pack = 4)]
onTwoStruct
seems to resolve this issue. – LabyrinthodontStructLayoutAttribute
could be added to a class until today! – Circumbendibus