数组是引用类型。所有引用类型都带有两个额外的单词字段。类型引用和 SyncBlock 索引字段,除其他外用于在 CLR 中实现锁。所以引用类型的类型开销是 32 位上的 8 个字节。最重要的是,数组本身还存储另外 4 个字节的长度。这使总开销达到 12 个字节。
我刚刚从 Jon Skeet 的回答中了解到,引用类型的数组有额外的 4 字节开销。这可以使用 WinDbg 来确认。事实证明,附加字是存储在数组中的类型的另一个类型引用。所有引用类型的数组都在内部存储为object[]
,并附加了对实际类型的类型对象的引用。所以 astring[]
实际上只是一个object[]
对 type 的附加类型引用string
。详情请见下文。
存储在数组中的值:引用类型的数组保存对对象的引用,因此数组中的每个条目都是引用的大小(即 32 位上的 4 个字节)。值类型的数组内联存储值,因此每个元素将占用相关类型的大小。
这个问题可能也很有趣:C# List<double> size vs double[] size
血腥细节
考虑以下代码
var strings = new string[1];
var ints = new int[1];
strings[0] = "hello world";
ints[0] = 42;
附加 WinDbg 显示以下内容:
首先让我们看一下值类型数组。
0:000> !dumparray -details 017e2acc
Name: System.Int32[]
MethodTable: 63b9aa40
EEClass: 6395b4d4
Size: 16(0x10) bytes
Array: Rank 1, Number of elements 1, Type Int32
Element Methodtable: 63b9aaf0
[0] 017e2ad4
Name: System.Int32
MethodTable 63b9aaf0
EEClass: 6395b548
Size: 12(0xc) bytes
(C:\Windows\assembly\GAC_32\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll)
Fields:
MT Field Offset Type VT Attr Value Name
63b9aaf0 40003f0 0 System.Int32 1 instance 42 m_value <=== Our value
0:000> !objsize 017e2acc
sizeof(017e2acc) = 16 ( 0x10) bytes (System.Int32[])
0:000> dd 017e2acc -0x4
017e2ac8 00000000 63b9aa40 00000001 0000002a <=== That's the value
首先我们转储数组和值为 42 的一个元素。可以看出大小为 16 字节。这是int32
值本身的 4 个字节,常规引用类型开销的 8 个字节和数组长度的另外 4 个字节。
原始转储显示 SyncBlock、方法表int[]
、长度和 42 的值(十六进制中的 2a)。请注意,SyncBlock 正好位于对象引用的前面。
接下来,让我们看一下,string[]
以找出附加词的用途。
0:000> !dumparray -details 017e2ab8
Name: System.String[]
MethodTable: 63b74ed0
EEClass: 6395a8a0
Size: 20(0x14) bytes
Array: Rank 1, Number of elements 1, Type CLASS
Element Methodtable: 63b988a4
[0] 017e2a90
Name: System.String
MethodTable: 63b988a4
EEClass: 6395a498
Size: 40(0x28) bytes <=== Size of the string
(C:\Windows\assembly\GAC_32\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll)
String: hello world
Fields:
MT Field Offset Type VT Attr Value Name
63b9aaf0 4000096 4 System.Int32 1 instance 12 m_arrayLength
63b9aaf0 4000097 8 System.Int32 1 instance 11 m_stringLength
63b99584 4000098 c System.Char 1 instance 68 m_firstChar
63b988a4 4000099 10 System.String 0 shared static Empty
>> Domain:Value 00226438:017e1198 <<
63b994d4 400009a 14 System.Char[] 0 shared static WhitespaceChars
>> Domain:Value 00226438:017e1760 <<
0:000> !objsize 017e2ab8
sizeof(017e2ab8) = 60 ( 0x3c) bytes (System.Object[]) <=== Notice the underlying type of the string[]
0:000> dd 017e2ab8 -0x4
017e2ab4 00000000 63b74ed0 00000001 63b988a4 <=== Method table for string
017e2ac4 017e2a90 <=== Address of the string in memory
0:000> !dumpmt 63b988a4
EEClass: 6395a498
Module: 63931000
Name: System.String
mdToken: 02000024 (C:\Windows\assembly\GAC_32\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll)
BaseSize: 0x10
ComponentSize: 0x2
Number of IFaces in IFaceMap: 7
Slots in VTable: 196
首先我们转储数组和字符串。接下来我们转储string[]
. 请注意,WinDbg 将类型列为System.Object[]
此处。在这种情况下,对象大小包括字符串本身,因此总大小是数组中的 20 加上字符串的 40。
通过转储实例的原始字节,我们可以看到以下内容:首先我们有 SyncBlock,然后遵循 的方法表object[]
,然后是数组的长度。之后,我们通过对字符串方法表的引用找到额外的 4 个字节。这可以通过如上所示的 dumpmt 命令来验证。最后,我们找到了对实际字符串实例的单一引用。
综上所述
数组的开销可以分解如下(在 32 位上)
- 4字节同步块
- 4 个字节用于数组本身的方法表(类型引用)
- 数组长度为 4 个字节
- 引用类型数组再增加 4 个字节来保存实际元素类型的方法表(引用类型数组
object[]
在底层)
即,值类型数组的开销为 12 个字节,引用类型数组的开销为 16 个字节。