这里是一个稍微整洁一些(我认为)的简短完整程序,用于演示同样的事情:
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[] 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,它们的值是:
- 同步块,用于锁定哈希码(或薄锁-请参见Brian的评论)
- 类型指针
- 数组的大小
- 元素类型指针
- 空引用(第一个元素)
对于z,其值为:
- 同步块
- 类型指针
- 数组的大小
- 0x12345678(第一个元素)
不同类型的值类型数组(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
这就是我之前提到的协方差。考虑到这将在
每个分配中发生,因此减少间接性很有意义。特别是,我怀疑您不希望通过每次分配都去获取元素类型来消耗缓存。我
猜测(我的x86汇编水平还不够好),该测试是这样的:
- 要复制的值是否为null引用?如果是,则没问题。(完成)
- 获取引用指向的对象的类型指针。
- 该类型指针与元素类型指针相同(简单的二进制相等检查)吗?如果是,则没问题。(完成)
- 该类型指针是否可以赋值给元素类型指针?(涉及继承和接口的更复杂检查) 如果是,则没问题-否则,抛出异常。
如果我们能在前三个步骤中终止搜索,那么就没有太多间接性——对于像数组分配这样经常发生的操作而言,这非常有利。对于值类型分配,无需进行任何操作,因为这是静态可验证的。
因此,这就是我认为引用类型数组稍微比值类型数组大的原因。
很棒的问题-真的很有趣 :)