Qwe*_*tie 36 .net c# arrays overhead
我试图使用以下代码确定.NET数组(在32位进程中)标头的开销:
long bytes1 = GC.GetTotalMemory(false);
object[] array = new object[10000];
for (int i = 0; i < 10000; i++)
array[i] = new int[1];
long bytes2 = GC.GetTotalMemory(false);
array[0] = null; // ensure no garbage collection before this point
Console.WriteLine(bytes2 - bytes1);
// Calculate array overhead in bytes by subtracting the size of
// the array elements (40000 for object[10000] and 4 for each
// array), and dividing by the number of arrays (10001)
Console.WriteLine("Array overhead: {0:0.000}",
((double)(bytes2 - bytes1) - 40000) / 10001 - 4);
Console.Write("Press any key to continue...");
Console.ReadKey();
Run Code Online (Sandbox Code Playgroud)
结果是
204800
Array overhead: 12.478
Run Code Online (Sandbox Code Playgroud)
在32位进程中,object [1]应该与int [1]的大小相同,但实际上开销会跳过3.28个字节到
237568
Array overhead: 15.755
Run Code Online (Sandbox Code Playgroud)
谁知道为什么?
(顺便说一句,如果有人好奇,非数组对象的开销,例如上面循环中的(对象)i,大约是8个字节(8.384).我听说它在64位进程中是16个字节.)
Jon*_*eet 46
这是一个稍微整洁(IMO)简短但完整的程序来演示同样的事情:
using System;
class Test
{
const int Size = 100000;
static void Main()
{
object[] array = new object[Size];
long initialMemory = GC.GetTotalMemory(true);
for (int i = 0; i < Size; i++)
{
array[i] = new string[0];
}
long finalMemory = GC.GetTotalMemory(true);
GC.KeepAlive(array);
long total = finalMemory - initialMemory;
Console.WriteLine("Size of each element: {0:0.000} bytes",
((double)total) / Size);
}
}
Run Code Online (Sandbox Code Playgroud)
但我得到相同的结果 - 任何引用类型数组的开销是16字节,而任何值类型数组的开销是12字节.在CLI规范的帮助下,我仍然试图弄清楚为什么会这样.不要忘记引用类型数组是协变的,这可能是相关的......
编辑:在cordbg的帮助下,我可以确认Brian的答案 - 无论实际的元素类型如何,引用类型数组的类型指针都是相同的.据推测,有一些funkiness object.GetType()
(非虚拟,记住)来解释这一点.
因此,代码为:
object[] x = new object[1];
string[] y = new string[1];
int[] z = new int[1];
z[0] = 0x12345678;
lock(z) {}
Run Code Online (Sandbox Code Playgroud)
我们最终得到如下内容:
Variables:
x=(0x1f228c8) <System.Object[]>
y=(0x1f228dc) <System.String[]>
z=(0x1f228f0) <System.Int32[]>
Memory:
0x1f228c4: 00000000 003284dc 00000001 00326d54 00000000 // Data for x
0x1f228d8: 00000000 003284dc 00000001 00329134 00000000 // Data for y
0x1f228ec: 00000000 00d443fc 00000001 12345678 // Data for z
Run Code Online (Sandbox Code Playgroud)
请注意,我在变量本身的值之前转储了内存1个字.
对于x
和y
,值为:
对于z
,值是:
不同的值类型数组(byte [],int []等)以不同的类型指针结束,而所有引用类型数组使用相同的类型指针,但具有不同的元素类型指针.元素类型指针与您找到的该类型对象的类型指针的值相同.因此,如果我们在上面的运行中查看字符串对象的内存,它的类型指针将为0x00329134.
该类型的指针之前,这个词肯定有事情做与液晶显示屏或哈希代码:调用GetHashCode()
填充的内存位,我相信默认object.GetHashCode()
获得同步块,以确保哈希代码的唯一性为对象的生命周期.然而,只是做lock(x){}
了没有做任何事情,这让我感到惊讶......
所有这些只对"矢量"类型有效,顺便说一句 - 在CLR中,"矢量"类型是一个下限为0的单维数组.其他数组将具有不同的布局 - 一方面,他们需要存储下限......
到目前为止,这已经是实验,但这是猜测 - 系统以其实现的方式实施的原因.从现在开始,我真的只是在猜测.
object[]
阵列都可以共享相同的JIT代码.它们在内存分配,数组访问,Length
属性和(重要的)GC的引用布局方面的行为方式相同.将其与值类型数组进行比较,其中不同的值类型可能具有不同的GC"足迹"(例如,一个可能有一个字节,然后是一个引用,其他的根本没有引用,等等).每次object[]
在运行时分配值时都需要检查它是否有效.它需要检查您用于新元素值的引用的对象类型是否与数组的元素类型兼容.例如:
object[] x = new object[1];
object[] y = new string[1];
x[0] = new object(); // Valid
y[0] = new object(); // Invalid - will throw an exception
Run Code Online (Sandbox Code Playgroud)这是我前面提到的协方差.现在考虑到每个单独的任务都会发生这种情况,减少间接数量是有意义的.特别是,我怀疑你真的不想通过转到每个分配的类型对象来获取元素类型来破坏缓存.我怀疑(并且我的x86程序集不够好以验证这一点)测试类似于:
如果我们可以在前三个步骤中终止搜索,那么就没有很多间接 - 这对于像阵列分配那样频繁发生的事情是有益的.对于值类型赋值,这一切都不需要发生,因为这是静态可验证的.
所以,这就是为什么我认为引用类型数组比值类型数组略大.
很棒的问题 - 真的很有趣深入研究:)
Bri*_*sen 23
Array是一种引用类型.所有引用类型都带有两个附加字段.类型引用和SyncBlock索引字段,其中用于在CLR中实现锁定.因此,引用类型的类型开销是32位上的8个字节.最重要的是,数组本身也存储了另外4个字节的长度.这使总开销达到12个字节.
我刚刚从Jon Skeet的回答中了解到,引用类型数组还有4个字节的开销.这可以使用WinDbg确认.事实证明,附加的单词是存储在数组中的类型的另一种类型引用.所有引用类型数组都在内部存储object[]
,并附加引用实际类型的类型对象.所以a string[]
实际上只是对object[]
类型的附加类型引用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;
Run Code Online (Sandbox Code Playgroud)
附加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
Run Code Online (Sandbox Code Playgroud)
首先,我们转储数组和值为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
Run Code Online (Sandbox Code Playgroud)
首先我们转储数组和字符串.接下来我们转储大小string[]
.请注意,WinDbg在System.Object[]
此处列出了类型.在这种情况下,对象大小包括字符串本身,因此总大小是数组中的20加上字符串的40.
通过转储实例的原始字节,我们可以看到以下内容:首先我们有SyncBlock,然后是方法表object[]
,然后是数组的长度.之后,我们通过对字符串的方法表的引用找到额外的4个字节.这可以通过dumpmt命令验证,如上所示.最后,我们找到对实际字符串实例的单引用.
结论
数组的开销可以按如下方式进行细分(在32位上)
object[]
在引擎盖下)即,开销值类型阵列的12个字节并为引用类型阵列的16个字节.