.NET自定义结构类型的内存开销是多少?

13

.NET对象存在固定的开销,具体细节在这个SO问题中有更详细的概述: 什么是.NET对象的内存开销,根据您所处的32位或64位进程的不同,其大小为12或24字节。

话虽如此,像int、double、boolean等基本值类型没有开销,因为它们是值类型

那么您在应用程序中定义的自定义struct类型属于哪一类呢?一方面,它们像上面提到的int、double、boolean一样是值类型[所以不应该产生开销],但另一方面,它们间接地派生自System.Object,因此(技术上)应该产生开销。


1
请注意,基元类型也间接地派生自 System.Object - 例如 System.Int32 : System.ValueType - Jon Skeet
1
结构体并不是真正的从对象派生出来的;它们通过一些编译器的技巧成功地假装自己是。所以所有的结构体都是隐式密封的,这对于编译器来说并不太困难(即需要跟踪一些方法)。 - Sergey Kalinichenko
4个回答

9
一个结构体的大小由其字段大小之和以及使它们正确对齐的字段之间的填充加上结构体末尾的填充决定,以确保在存储结构体时仍然正确对齐。因此,结构体不太可能完全不包含引用类型的字段。比如字符串。此时,结构体会变得更大,因为引用在幕后是指针,占据 8 个字节而不是 4 个字节。
填充是一个更加棘手的细节。在 32 位模式下,变量不能保证有一个更好的对齐方式,只能保证 4 字节。如果使用 double 和 long 这样的 8 字节类型且未正确对齐,就会出现问题。这显著影响 32 位程序的性能。如果 double 跨越了 L1 缓存边界线而发生了错误的对齐,那么读取或写入速度可能会慢三倍。这也是这些类型在 C# 内存模型中不是“原子”的核心原因。在 64 位模式下不存在该问题,CLR 必须并且确实提供了 8 的对齐保证。
尽管如此,在 32 位模式下,CLR 会尝试为这些结构体成员提供适当的对齐方式,即使结构体本身不能保证对齐。否则,结构体会有一个隐式的 [StructLayout(LayoutKind.Sequential, Pack=8)] 属性副作用。这是 CLR 源代码中的一个怪异之处,执行此操作的 C++ 语句没有评论。我猜想这是为了解决不太理想的非托管互操作性能而进行的快速修复,保持结构体可映射性对于速度来说非常重要。
然而,并非总是如此,如果结构体包含一个自身不具有顺序布局的结构体成员,则 CLR 将放弃这样做。特别是对于 DateTime 和 DateTimeOffset,编写它们的程序员在它们上面应用了 [StructLayout(LayoutKind.Auto)] 属性,原因非常神秘。对于 DateTimeOffset,可能是复制/粘贴错误。现在,结构体的布局将变得不可预测,它也变成 LayoutKind.Auto,并且 CLR 重新排列字段以最小化结构体大小。这可能会导致 x64 模式下额外的填充。
这些是你永远不需要担心的晦涩实现细节。

8

那么,你在应用程序中组合的自定义结构类型又在哪里呢?

它们与基元类型没有区别。除了它们具有的字段之外,它们不会承载任何额外的开销。它们从 object 派生并不意味着它们会产生引用类型所承载的开销,这是方法表指针和同步根块。

您可以使用 Marshal.SizeOf 进行测试:

void Main()
{
    var f = new Foo();
    Console.WriteLine(Marshal.SizeOf(f));
}

public struct Foo
{
    public int X { get; set; }
    public int Y { get; set; }
}

这将在32位模式下打印8,这恰好是两个整数值(每个4字节)。请注意,Marshal.SizeOf将输出未托管对象的大小。CLR仍然可以自由地重新排序字段或将它们打包在一起。

如果您将Foo声明为class,那么Marshal.SizeOf会返回不同的大小吗? - user4003407
@PetSerAl 当你使用 StructLayout 时它会工作,但它不会给你对象的实际大小。 - Yuval Itzchakov
但是你将它用作证明“struct”没有开销。那么,如果它不是“class”的实际大小,为什么它是“struct”的实际大小呢? - user4003407
2
如果需要将结构体进行封送到非托管代码(例如PInvoke),则Marshal.SizeOf将为您提供结构体的大小,这并不一定与CLR使用的大小相同。 - Justin
@Justin 它将为您提供未托管对象的大小。CLR 可能选择重新排序字段或将它们打包在一起,因此这对于内存中的对象不是100%准确的。 - Yuval Itzchakov
显示剩余4条评论

3

1

零。

除非由于对齐而存在间隙,否则它就是这些间隙的成本。

但是,否则结构与如果这些字段是分开的变量并排放置在堆栈上是相同的。

将其装箱,然后通过 object 处理,这与任何其他引用类型具有相同的开销。


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