Struct alignment inside a class in .NET Core
Asked Answered
B

1

11

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?

Boyer answered 19/10, 2021 at 13:10 Comment(4)
Using [StructLayoutAttribute(LayoutKind.Sequential, Pack = 4)] on TwoStruct seems to resolve this issue.Labyrinthodont
@GuruStron I didn't know that StructLayoutAttribute could be added to a class until today!Circumbendibus
@Circumbendibus found out it myself recently in the docs =)Labyrinthodont
Unless you want to achieve interoperability with other languages (like C/C++ using P/Invoke), or with flat file records, etc. there's no particular reason to define StructLayout. https://mcmap.net/q/17221/-purpose-of-memory-alignmentEnthuse
H
7

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.

Horoscope answered 26/10, 2021 at 9:16 Comment(3)
"For some reason unknown to me, it aligns fields of custom value types at a pointer size boundary" - the reason is called performance. Reads/Writes happen on those levels. They are not pointer size boundaries, pointers just happen to ALSO use the same size these days. SOME processors back long time ago did not (like a famous 16/32 bit which has 16 bit word size but internal 32 bit processing, iirc, or was it 8/16--- that is like 40 years ago.Kilowatthour
@Kilowatthour but why won't it align 'int' fields on the same boundaries then? Why custom structs only?Horoscope
@Horoscope thank you. The main thing I was missing was that default memory layout differs for classes and structs. Not sure how I missed that in the docs)Labyrinthodont

© 2022 - 2024 — McMap. All rights reserved.