Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Struct alignment inside a class in .NET Core

I'm trying to understand why a struct that contains only int takes 8 bytes of memory inside a class.

considering the following code;

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;
    }
}

now, when I'm profiling this two instances with dotMemory i get the following result:

enter image description here

Although both int and the intStruct take 4 bytes of memory on the stack, it looks like the class size on the heap is different and that struct is always aligned to 8 bytes.

What can cause this behavior?

like image 690
Amir Harari Avatar asked Oct 19 '21 13:10

Amir Harari


1 Answers

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.

like image 72
Evk Avatar answered Sep 28 '22 03:09

Evk