我试图使用以下代码确定.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();
结果是
204800 Array overhead: 12.478
在32位进程中,object [1]的大小应与int [1]相同,但是实际上开销跳升了3.28个字节,
237568 Array overhead: 15.755
有人知道为什么吗?
(顺便说一句,如果有人好奇的话,非数组对象(例如,上面循环中的(object)i)的开销约为8个字节(8.384)。我听说在64位进程中为16个字节。)
这是一个稍微整洁的(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); } }
但是我得到了相同的结果- 任何引用类型数组的开销都是16个字节,而任何值类型数组的开销都是12个字节。我仍然在CLI规范的帮助下尝试找出原因。不要忘记引用类型数组是协变的,这可能是相关的…
编辑:借助cordbg,我可以确认Brian的答案- 引用类型数组的类型指针是相同的,而不管实际的元素类型是什么。大概是在某种程度上object.GetType()(不是虚拟的,请记住)来解决这个问题。
object.GetType()
因此,使用以下代码:
object[] x = new object[1]; string[] y = new string[1]; int[] z = new int[1]; z[0] = 0x12345678; lock(z) {}
我们最终得到如下内容:
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
请注意,我已经在变量本身的值 之前 转储了1个字。
对于x和y,值是:
x
y
对于z,值是:
z
不同的值类型数组(byte [],int []等)最终具有不同的类型指针,而所有引用类型数组都使用相同的类型指针,但具有不同的元素类型指针。元素类型指针的值与该类型对象的类型指针的值相同。因此,如果我们在上面的运行中查看了字符串对象的内存,它的类型指针将为0x00329134。
该类型的指针之前,这个词肯定有 事情 做与液晶显示屏或哈希代码:调用GetHashCode()该位的内存填充,我相信默认object.GetHashCode()获得同步块,以确保哈希代码的唯一性为对象的生命周期。但是,lock(x){}做任何事情都没做,这让我感到惊讶。
GetHashCode()
object.GetHashCode()
lock(x){}
顺便说一下,所有这些仅对“向量”类型有效-在CLR中,“向量”类型是下限为0的一维数组。其他数组将具有不同的布局-一方面,他们需要存储下限…
到目前为止,这只是实验,但这只是猜测-系统以其现有方式实施的原因。从这里开始,我真的只是在猜测。
object[]
Length
object[] x = new object[1];
object[] y = new string[1]; x[0] = new object(); // Valid y[0] = new object(); // Invalid - will throw an exception
这就是我前面提到的协方差。现在,考虑到 每个分配 都会发生这种情况,减少间接数是有意义的。特别是,我怀疑您真的不需要通过每次分配的类型对象都获取元素类型来破坏缓存。我 怀疑 (我的x86程序集不足以验证这一点)测试是否像这样:
如果我们可以在前三个步骤中终止搜索,则没有太多的间接操作-这对于像数组分配这样频繁发生的事情很有用。对于值类型分配,这些都不需要发生,因为这是静态可验证的。
因此,这就是为什么我认为引用类型数组比值类型数组稍大的原因。
好问题-深入研究真的很有趣:)