【问题标题】:Struct alignment inside a class in .NET Core.NET Core 类中的结构对齐
【发布时间】:2023-03-04 10:19:01
【问题描述】:

我试图理解为什么一个只包含 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 分析这两个实例时,我得到以下结果:

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

什么会导致这种行为?

【问题讨论】:

  • TwoStruct 上使用[StructLayoutAttribute(LayoutKind.Sequential, Pack = 4)] 似乎可以解决这个问题。
  • @GuruStron 直到今天我才知道StructLayoutAttribute 可以添加到课程中!
  • @Sweeper 最近在docs 中发现了它=)
  • 除非您想实现与其他语言的互操作性(例如使用 P/Invoke 的 C/C++),或者与平面文件记录等,否则没有特别的理由来定义 StructLayout。 stackoverflow.com/questions/381244/purpose-of-memory-alignment

标签: c# .net-core memory struct memory-alignment


【解决方案1】:

对于类,默认内存布局为“自动”,这意味着 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 不会在指针大小边界处对齐它。如果我们使用IntStruct 而不是LongStructlong 而不是int - 那么这两种情况将具有相同的大小,因为long 是8 个字节,即使对于自定义结构CLR 也不需要添加任何填充将其对齐在 64 位中的 8 字节边界。

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

如果类型布局是 LayoutKind.Auto,CLR 将填充每个字段 自定义值类型!这意味着如果您有多个结构 只包装一个 int 或 byte,它们被广泛用于数百万 对象,由于填充,您可能会有明显的内存开销!

can affect 使用 StructLayouAttributeLayoutKind = Sequential 的类的托管布局如果这个类中的所有字段都是 blittable (在这个问题中就是这种情况):

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

如 cmets 中所述,我们可以通过以下方式移除填充:

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

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

【讨论】:

  • “出于某种我不知道的原因,它在指针大小边界处对齐自定义值类型的字段” - 原因称为性能。读/写发生在这些级别上。它们不是指针大小的界限,指针只是碰巧现在也使用相同的大小。很久以前的一些处理器没有(比如著名的 16/32 位,它有 16 位字长,但内部有 32 位处理,iirc,或者是 8/16 ——就像 40 年前一样。
  • @TomTom 但为什么不在同一边界上对齐“int”字段呢?为什么只使用自定义结构?
  • @Evk 谢谢。我缺少的主要内容是类和结构的默认内存布局不同。不知道我是如何在文档中错过的)
猜你喜欢
  • 1970-01-01
  • 2018-02-24
  • 1970-01-01
  • 2020-11-17
  • 2012-05-02
  • 2014-02-19
  • 1970-01-01
  • 2019-12-20
相关资源
最近更新 更多