.NET Core 中类内部的结构对齐

Ami*_*ari 10 c# memory struct memory-alignment .net-core

我试图理解为什么只包含 int 的结构在类中占用 8 个字节的内存。

考虑以下代码;

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;
    }
}
Run Code Online (Sandbox Code Playgroud)

现在,当我使用 dotMemory 分析这两个实例时,我得到以下结果:

在此输入图像描述

尽管 int 和 intStruct 都在堆栈上占用 4 个字节的内存,但看起来堆上的类大小不同,并且该结构始终与 8 个字节对齐。

什么会导致这种行为?

Evk*_*Evk 7

对于类,默认内存布局是“自动”,这意味着 CLR 自行决定如何在内存中对齐类中的字段。这是一个未记录的实现细节。由于某种我不知道的原因,它将自定义值类型的字段对齐在指针大小边界(因此,64 位进程中为 8 个字节,32 位进程中为 4 个字节)。

\n

如果您以 32 位编译该代码,您将看到TwoIntTwoStruct现在都占用 16 个字节(4 个用于对象头,4 个用于方法表指针,然后 8 个用于字段),因为现在它们在 4 字节边界对齐。

\n

在 64 位情况下,就像您的问题一样,自定义值类型在 8 字节边界对齐,因此TwoStruct布局为:

\n
Object Header (8 bytes)\nMethod Table Pointer (8 bytes)\nIntStruct A (4 bytes)\npadding (4 bytes, to align at 8 bytes)\nIntStruct B (4 bytes)\npadding (4 bytes)\n
Run Code Online (Sandbox Code Playgroud)\n

并且TwoInt只是

\n
Object Header (8 bytes)\nMethod Table Pointer (8 bytes)\nIntStruct A (4 bytes)\nIntStruct B (4 bytes)\n
Run Code Online (Sandbox Code Playgroud)\n

因为int不是自定义值类型 - CLR 不会将其对齐到指针大小边界。如果IntStruct我们使用LongStructandlong而不是int- 那么两种情况将具有相同的大小,因为long是 8 字节,甚至对于自定义结构 CLR 也不需要添加任何填充来将其对齐在 64 位中的 8 字节边界。

\n

这是与该问题相关的一篇有趣的文章。作者开发了非常有趣的工具来直接从 .NET 代码检查对象的内存布局(无需外部工具)。他研究了同样的问题并得出了上述结论:

\n
\n

如果类型布局是 LayoutKind.Auto,则 CLR 将填充\n自定义值类型的每个字段!这意味着,如果您有多个结构\n仅包装单个 int 或字节,并且它们\xe2\x80\x99 广泛用于数百万个\n对象,则由于填充,您可能会产生明显的内存开销!

\n
\n

可以通过 IF 类中的所有字段都是 blittable 来影响StructLayouAttribute类的托管布局LayoutKind = Sequential(本问题就是这种情况):

\n
\n

对于 blittable 类型,LayoutKind.Sequential 控制托管内存中的布局和非托管内存中的布局。对于非 blittable 类型,它控制将类或结构编组到非托管代码时的布局,但不控制托管内存中的布局

\n
\n

因此,正如评论中提到的,我们可以通过执行以下操作来删除填充:

\n
[StructLayoutAttribute(LayoutKind.Sequential, Pack = 4)]\npublic class TwoStruct\n{\n    private readonly IntStruct A;\n    private readonly IntStruct B;\n\n    public TwoStruct(\n        IntStruct a,\n        IntStruct b)\n    {\n        A = a;\n        B = b;\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n

这实际上会节省我们一些内存。

\n