EDIT2
struct RefAndTwoInt32Wrappers
{
public int x;
public string s;
}
这段代码将会进行8字节对齐,因此结构体将会占用16字节。相比之下,这样的代码:
struct RefAndTwoInt32Wrappers
{
public int x,y;
public string s;
}
将会对齐到4字节,所以这个结构体也将有16字节。因此,在CLR中,结构体的对齐方式取决于最具对齐性的字段数,而类显然不能做到这一点,因此它们将保持8字节的对齐方式。
现在,如果我们将所有内容组合起来并创建结构体:
struct RefAndTwoInt32Wrappers
{
public int x,y;
public Int32Wrapper z;
public string s;
}
它将拥有24字节,其中{x,y}将每个具有4字节,{z,s}将每个具有8字节。一旦我们在结构体中引入引用类型,CLR将始终对齐我们的自定义结构以匹配类对齐。
struct RefAndTwoInt32Wrappers
{
public Int32Wrapper z;
public long l;
public int x,y;
}
这段代码的大小为24字节,因为Int32Wrapper将与long对齐。因此,自定义结构包装器将始终与结构中最高/最佳对齐的字段对齐,或者对齐到其自身内部最重要的字段。因此,在ref string对齐为8字节的情况下,结构包装器将对齐到它上面。
总之,结构中的自定义结构字段始终会对齐到结构中最高对齐的实例字段。现在,如果我不确定这是否是一个错误,但没有证据,我将坚持我的观点认为这可能是有意的决定。
编辑
只有在堆上分配时,大小才准确,但结构本身具有更小的大小(其字段的确切大小)。 进一步的分析似乎表明这可能是CLR代码中的错误,但需要有证据来支持。
如果发现有用的内容,我将检查cli代码并发布进一步的更新。
这是.NET内存分配器使用的对齐策略。
public static RefAndTwoInt32s[] test = new RefAndTwoInt32s[1];
static void Main()
{
test[0].text = "a";
test[0].x = 1;
test[0].x = 1;
Console.ReadKey();
}
这段代码使用 .net40 编译,在 x64 下运行。在 WinDbg 中,我们可以执行以下操作:
首先找到堆中的类型:
0:004> !dumpheap -type Ref
Address MT Size
0000000003e72c78 000007fe61e8fb58 56
0000000003e72d08 000007fe039d3b78 40
Statistics:
MT Count TotalSize Class Name
000007fe039d3b78 1 40 RefAndTwoInt32s[]
000007fe61e8fb58 1 56 System.Reflection.RuntimeAssembly
Total 2 objects
有了它后,让我们看看该地址下面的内容:
0:004> !do 0000000003e72d08
Name: RefAndTwoInt32s[]
MethodTable: 000007fe039d3b78
EEClass: 000007fe039d3ad0
Size: 40(0x28) bytes
Array: Rank 1, Number of elements 1, Type VALUETYPE
Fields:
None
我们可以看到这是一个值类型,并且这是我们创建的类型。由于这是一个数组,我们需要获取数组中单个元素的值类型定义:
我们发现这是一个值类型,而且是我们自己创建的。由于这是一个数组,我们需要获取数组中单个元素的值类型定义:
0:004> !dumparray -details 0000000003e72d08
Name: RefAndTwoInt32s[]
MethodTable: 000007fe039d3b78
EEClass: 000007fe039d3ad0
Size: 40(0x28) bytes
Array: Rank 1, Number of elements 1, Type VALUETYPE
Element Methodtable: 000007fe039d3a58
[0] 0000000003e72d18
Name: RefAndTwoInt32s
MethodTable: 000007fe039d3a58
EEClass: 000007fe03ae2338
Size: 32(0x20) bytes
File: C:\ConsoleApplication8\bin\Release\ConsoleApplication8.exe
Fields:
MT Field Offset Type VT Attr Value Name
000007fe61e8c358 4000006 0 System.String 0 instance 0000000003e72d30 text
000007fe61e8f108 4000007 8 System.Int32 1 instance 1 x
000007fe61e8f108 4000008 c System.Int32 1 instance 0 y
这个结构实际上是32字节,因为它的16字节被保留用于填充,所以从一开始每个结构体的大小至少为16字节。
如果你从int和一个字符串引用中添加16个字节到:0000000003e72d18 + 8个字节EE/填充,你会到达0000000003e72d30,这是字符串引用的起始点,由于所有引用都从它们的第一个实际数据字段开始填充了8个字节,这就弥补了我们这个结构体的32个字节。
让我们看看这个字符串是否实际上是按照这种方式填充的:
0:004> !do 0000000003e72d30
Name: System.String
MethodTable: 000007fe61e8c358
EEClass: 000007fe617f3720
Size: 28(0x1c) bytes
File: C:\WINDOWS\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
String: a
Fields:
MT Field Offset Type VT Attr Value Name
000007fe61e8f108 40000aa 8 System.Int32 1 instance 1 m_stringLength
000007fe61e8d640 40000ab c System.Char 1 instance 61 m_firstChar
000007fe61e8c358 40000ac 18 System.String 0 shared static Empty
>> Domain:Value 0000000001577e90:NotInit <<
现在让我们以同样的方式分析上面的程序:
public static RefAndTwoInt32Wrappers[] test = new RefAndTwoInt32Wrappers[1];
static void Main()
{
test[0].text = "a";
test[0].x.x = 1;
test[0].y.x = 1;
Console.ReadKey();
}
0:004> !dumpheap -type Ref
Address MT Size
0000000003c22c78 000007fe61e8fb58 56
0000000003c22d08 000007fe039d3c00 48
Statistics:
MT Count TotalSize Class Name
000007fe039d3c00 1 48 RefAndTwoInt32Wrappers[]
000007fe61e8fb58 1 56 System.Reflection.RuntimeAssembly
Total 2 objects
我们的结构现在是48字节。
0:004> !dumparray -details 0000000003c22d08
Name: RefAndTwoInt32Wrappers[]
MethodTable: 000007fe039d3c00
EEClass: 000007fe039d3b58
Size: 48(0x30) bytes
Array: Rank 1, Number of elements 1, Type VALUETYPE
Element Methodtable: 000007fe039d3ae0
[0] 0000000003c22d18
Name: RefAndTwoInt32Wrappers
MethodTable: 000007fe039d3ae0
EEClass: 000007fe03ae2338
Size: 40(0x28) bytes
File: C:\ConsoleApplication8\bin\Release\ConsoleApplication8.exe
Fields:
MT Field Offset Type VT Attr Value Name
000007fe61e8c358 4000009 0 System.String 0 instance 0000000003c22d38 text
000007fe039d3a20 400000a 8 Int32Wrapper 1 instance 0000000003c22d20 x
000007fe039d3a20 400000b 10 Int32Wrapper 1 instance 0000000003c22d28 y
在这种情况下,如果我们将0000000003c22d18的8个字节的字符串引用添加到中,我们将最终到达第一个Int包装器的开头,该值实际上指向我们所在的地址。
现在我们可以看到每个值都是一个对象引用,让我们通过窥视0000000003c22d20来确认。
0:004> !do 0000000003c22d20
<Note: this object has an invalid CLASS field>
Invalid object
因为它是一个结构体,所以地址并不能告诉我们这是一个对象还是虚函数表,实际上这是正确的。
0:004> !dumpvc 000007fe039d3a20 0000000003c22d20
Name: Int32Wrapper
MethodTable: 000007fe039d3a20
EEClass: 000007fe03ae23c8
Size: 24(0x18) bytes
File: C:\ConsoleApplication8\bin\Release\ConsoleApplication8.exe
Fields:
MT Field Offset Type VT Attr Value Name
000007fe61e8f108 4000001 0 System.Int32 1 instance 1 x
实际上,这更像是一个Union类型,这次它将被8字节对齐(所有填充都将与父结构体对齐)。如果不是这样,我们最终会得到20字节,这并不是最优的,因此内存分配器不会允许发生这种情况。如果您再次计算,就会发现该结构确实是40字节大小。
如果您想更节约内存,您不应该打包在自定义结构体类型中,而是使用简单的数组。另一种方法是从堆外分配内存(例如VirtualAllocEx),以这种方式,您将获得自己的内存块,并且可以按照您想要的方式管理它。
最后一个问题是,为什么突然会出现这样的布局。如果你比较int[]增量和struct[]与计数器字段增量的jitted代码和性能,第二个将生成一个8字节对齐的地址作为union,但当jitted时,这将转换为更优化的汇编代码(单个LEA vs多个MOV)。然而,在这里描述的情况下,性能实际上会更差,所以我的看法是,这与底层CLR实现相一致,因为它是一个可以有多个字段的自定义类型,因此可能更容易/更好地放置起始地址而不是数值(因为这是不可能的),并在那里进行struct填充,从而导致更大的字节大小。
TwoInt32Wrappers
或者一个Int64
和一个TwoInt32Wrappers
的结构体,会发生什么?如果您创建一个泛型Pair<T1,T2> {public T1 f1; public T2 f2;}
,然后创建Pair<string,Pair<int,int>>
和Pair<string,Pair<Int32Wrapper,Int32Wrapper>>
,会发生什么?哪些组合会强制 JITter 填充数据? - supercatPair<string, TwoInt32Wrappers>
确实只需要16个字节,这可以解决问题。有趣。 - Jon SkeetMarshal.SizeOf
会返回将要传递给本地代码的结构的大小,这个大小可能和.NET代码中的结构大小没有任何关系。 - supercat