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

10

我正在尝试理解为什么一个只包含 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;
    }
}

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

enter image description here

虽然int和intStruct在堆栈上都占用4字节的内存,但看起来它们在堆上的类大小是不同的,并且结构体始终对齐到8字节。

是什么导致了这种行为?


3
TwoStruct上使用[StructLayoutAttribute(LayoutKind.Sequential, Pack = 4)]似乎可以解决这个问题。 - Guru Stron
3
我不知道StructLayoutAttribute可以被添加到类中,直到今天! - Sweeper
1
@Sweeper 最近我在 docs 中发现了这个信息 =) - Guru Stron
除非您想要与其他语言(如使用P/Invoke的C/C++)或平面文件记录实现互操作性等,否则没有特定的理由定义StructLayout。https://dev59.com/G3RC5IYBdhLWcg3wMeBS - Simon Mourier
1个回答

7
对于类,默认的内存布局是“自动”的,这意味着CLR决定如何在内存中对齐类中的字段。这是一种未记录的实现细节。出于我不知道的某种原因,它会将自定义值类型的字段对齐到指针大小的边界(64位进程为8字节,32位为4字节)。
如果您在32位编译该代码,则会发现TwoIntTwoStruct现在都需要占用16字节(对象头4字节,方法表指针4字节,然后是8字节的字段),因为现在它们被对齐到4字节的边界上了。
在64位情况下,就像你的问题一样,自定义值类型在8字节边界上对齐,因此TwoStruct的布局为:
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)

TwoInt 就是

Object Header (8 bytes)
Method Table Pointer (8 bytes)
IntStruct A (4 bytes)
IntStruct B (4 bytes)

因为int不是自定义值类型,CLR不会将其对齐到指针大小边界。如果我们使用LongStructlong代替IntStructint,那么两种情况的大小将相同,因为long是8字节,即使对于自定义结构体,CLR也不需要添加任何填充以将其对齐到64位的8字节边界。
这里有一篇有趣的文章与此问题相关。作者开发了一个非常有趣的工具,可以直接从.NET代码中检查对象的内存布局(无需外部工具)。他调查了这个问题,并得出了上面的结论:

如果类型布局是LayoutKind.Auto,则CLR会为每个自定义值类型的字段添加填充!这意味着,如果您有多个仅包装单个int或byte的结构体,并且它们在数百万个对象中广泛使用,由于填充,您可能会有明显的内存开销!

如果这个类中的所有字段都是可引用类型,那么您可以使用StructLayouAttribute以及LayoutKind = Sequential来影响该类的管理布局(在本问题中就是这种情况):

对于可引用类型,LayoutKind.Sequential 控制受控内存和非托管内存的布局。对于不可引用类型,它控制了在将类或结构转换为非托管代码时的布局,但并不控制受控内存中的布局。

因此,如评论所述,我们可以通过以下方式消除填充:

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

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


1
由于性能原因,它将自定义值类型的字段对齐到指针大小边界,这是我不知道的原因。读/写操作发生在这些级别上。它们不是指针大小的边界,只是现在指针也恰好使用相同的大小。一些处理器很久以前没有这样做(例如著名的16/32位处理器,其具有16位字长但内部处理为32位,如果我没记错的话,或者是8/16位---那是40年前的事了)。 - TomTom
@TomTom,为什么它不会将“int”字段对齐到相同的边界上呢?为什么只有自定义结构体? - Evk
@Evk 谢谢。我错过的主要事情是类和结构体的默认内存布局不同。不确定我在文档中是如何错过这一点的)。 - Guru Stron

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接