.NET数组的开销?

我试图使用这段代码来确定.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); } } 

但是我得到了相同的结果 – 任何引用types数组的开销是16字节,而任何值types数组的开销是12字节。 我仍然试图在CLI规范的帮助下解决这个问题。 不要忘记引用types的数组是协变的,这可能是相关的…

编辑:在cordbg的帮助下,我可以证实Brian的答案 – 引用types数组的types指针是相同的,无论实际的元素types。 大概在object.GetType() (这是非虚拟的,记得)有一些funkiness来说明这一点。

所以,代码如下:

 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 

请注意,我已经将variables本身的值之前的内存1个字丢弃了。

对于xy ,值是:

  • 同步块,用于locking哈希码(或一个薄锁 – 请参阅Brian的评论)
  • types指针
  • 数组的大小
  • 元素types指针
  • 空引用(第一个元素)

对于z ,值是:

  • 同步块
  • types指针
  • 数组的大小
  • 0x12345678(第一个元素)

不同的值types数组(byte [],int []等)以不同的types指针结束,而所有引用types数组使用相同的types指针,但具有不同的元素types指针。 元素types指针的值与您find的该types对象的types指针的值相同。 所以,如果我们在上面的运行中查看一个string对象的内存,它将有一个types指针0x00329134。

types指针之前的单词肯定与监视器或哈希代码有关:调用GetHashCode()填充该位内存,并且我相信默认的object.GetHashCode()获得一个同步块来确保哈希代码的唯一性对象的生命周期。 然而,只是做lock(x){}没有做任何事情,这让我感到惊讶…

所有这些只对“向量”types有效,顺便说一下,在CLR中,“向量”types是一个下界为0的单维数组。其他数组将有不同的布局 – ,他们需要存储的下限…

到目前为止,这已经是实验了,但是这是猜测 – 系统按照它的方式被执行的原因。 从这里开始,我真的只是在猜测。

  • 所有object[]数组可以共享相同的JIT代码。 就内存分配,数组访问, Length属性以及(重要的)GC引用的布局而言,它们的行为方式是相同的。 与值types数组比较,其中不同的值types可能有不同的GC“脚印”(例如,可能有一个字节,然后引用,其他人将根本没有引用,等等)。
  • 每次在一个object[]分配一个值时,运行时需要检查它是否有效。 它需要检查你为新元素值引用的对象的types是否与数组的元素types兼容。 例如:

     object[] x = new object[1]; object[] y = new string[1]; x[0] = new object(); // Valid y[0] = new object(); // Invalid - will throw an exception 

这是我前面提到的协方差。 现在考虑到每一项任务都会发生这种情况,减less间接数量是有意义的。 特别是,我怀疑你并不是真的想通过去每个分配的types对象来获取元素types来cachingcaching。 我怀疑 (和我的x86程序集不够好,以validation这一点),testing是这样的:

  • 被复制的值是否为空引用? 如果是这样,那很好。 (完成)。
  • 取参考点所在对象的types指针。
  • 该types的指针是否与元素types指针相同(简单的二进制相等性检查)? 如果是这样,那很好。 (完成)。
  • 这种types的指针是否与元素types指针兼容? (更复杂的检查,涉及inheritance和接口。)如果是这样,那很好 – 否则,抛出exception。

如果我们能够在前三个步骤中终止search,那么就没有太多的间接寻址 – 这对于像数组赋值那样经常发生的事情是有好处的。 这不需要为值types分配发生,因为这是静态可validation的。

所以,这就是为什么我认为引用types数组比数值types数组略大一些。

伟大的问题 – 深入研究它真的很有趣:)

数组是一个引用types。 所有参考types都带有两个额外的字段。 types引用和一个SyncBlock索引字段,除了别的以外,它用于在CLR中实现locking。 所以引用types的types开销是32位的8个字节。 最重要的是,数组本身也存储了另外4个字节的长度。 这使总开销达到12个字节。

我刚刚从Jon Skeet的回答中了解到,引用types数组有额外的4字节开销。 这可以使用WinDbg来确认。 事实certificate,额外的单词是存储在数组中的types的另一个types引用。 所有引用types的数组在内部存储为object[] ,并附加引用实际types的types对象。 所以一个string[]实际上只是一个object[]并带有对string的附加types引用。 有关详情,请参阅下文。

存储在数组中的值:引用types的数组保存对对象的引用,因此数组中的每个条目都是引用的大小(即32位上的4个字节)。 值types的数组将存储内联值,因此每个元素将占用相关types的大小。

这个问题也许是有趣的: 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显示以下内容:

首先让我们来看看值types数组。

 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个字节,对于常规引用types开销是8个字节,对于数组长度是4个字节。

原始转储显示了SyncBlock, int[]的方法表,长度以及值42(hex2a)。 请注意,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。 接下来我们转储string[]的大小。 注意,WinDbg在这里列出types为System.Object[] 。 在这种情况下,对象大小包含string本身,所以总大小是数组中的20,加上string的40。

通过转储实例的原始字节,我们可以看到以下内容:首先,我们有了SyncBlock,然后遵循object[]的方法表,然后是数组的长度。 在那之后,我们通过引用string的方法表来find额外的4个字节。 这可以通过dumpmt命令validation,如上所示。 最后我们find实际string实例的单引用。

结论是

数组的开销可以分解如下(在32位上)

  • 4个字节的SyncBlock
  • 方法表(types引用)为数组本身的4个字节
  • 数组长度为4个字节
  • 引用types数组增加了另外4个字节来存放实际元素types的方法表(引用types下的引用types数组是object[]

即开销是值types数组12个字节引用types数组的16个字节

我认为你在测量的时候做了一些错误的假设,因为你的循环期间的内存分配(通过GetTotalMemory)可能不同于数组的实际需要的内存 – 内存可能被分配在更大的块中,在循环中回收的内存等

这里有一些关于数组开销的信息:

  • arrays没有文件
  • 文章由杰弗里·里希特
  • .Nettypes的内部

由于堆pipe理(因为您处理GetTotalMemory)只能分配相当大的块,后者由CLR为程序员分配更小的块。

我很抱歉,但我在今天早上发现了一些关于记忆斩首的有趣信息。

我们有一个运行大量数据(高达2GB)的项目。 作为主要的存储我们使用Dictionary<T,T> 。 实际上创build了数千个字典。 将其更改为List<T>键和List<T>作为值(我们自己实现了IDictionary<T,T> )后,内存使用量减less了大约30-40%。

为什么?