我试图使用以下代码确定 .NET 数组(在 32 位进程中)上 header 的开销:
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[] 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
,值为:对于
z
,值为:不同的值类型数组(byte[]、int[] 等)最终具有不同的类型指针,而所有引用类型数组都使用相同的类型指针,但具有不同的元素类型指针。元素类型指针与您在该类型对象的类型指针中找到的值相同。因此,如果我们在上面的运行中查看字符串对象的内存,它将具有 0x00329134 的类型指针。
类型指针之前的单词肯定与监视器或哈希码有关:调用
GetHashCode()
会填充该位内存,我相信默认的 object.GetHashCode()
会获得一个同步块(synchronized block),以确保哈希码在对象的生命周期内是唯一的。然而,只是做 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 程序集不足以验证这一点)该测试类似于:
如果我们可以在前三个步骤中终止搜索,则不会有太多的间接性——这对于像数组赋值一样经常发生的事情是有好处的。对于值类型分配,这一切都不需要发生,因为这是静态可验证的。
所以,这就是为什么我认为引用类型数组比值类型数组稍大的原因。
很好的问题 - 深入研究它真的很有趣:)
关于c# - .NET 数组的开销?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/1589669/