首先,由于 C# 级别的分析只会显示执行时间最长的 C# 代码行,这当然是内联数组初始化,所以我们无法从中获得任何信息。但为了好玩,我们还是来分析一下:
![Profiling Results](https://istack.dev59.com/1m9VN.webp)
现在,当我们看到预期的结果时,让我们观察一下IL级别的代码,并尝试看看两个数组初始化之间的区别:
现在我们将关注剩下的两行:
第一行代码(L_001B
)加载了一些编译时类型,其类型名称为__StaticArrayInitTypeSize=16
,字段名为1456763F890A84558F99AFA687C36B9037697848
,并且它在名为<PrivateImplementationDetails>
的类中,该类位于Root Namespace
中。如果我们查看这个字段,我们会发现它完全包含了所需的数组,就像我们想要的那样将其编码为字节:
.field assembly static initonly valuetype <PrivateImplementationDetails>/__StaticArrayInitTypeSize=16 1456763F890A84558F99AFA687C36B9037697848 = ((01 00 00 00 02 00 00 00 03 00 00 00 04 00 00 00))
第二行调用一个方法,该方法使用我们刚在
L_0060
中创建的空数组和这个
编译时类型返回初始化的数组。
如果我们尝试查看此方法的代码,我们将看到它是
在CLR内部实现的。
[MethodImpl(MethodImplOptions.InternalCall), SecuritySafeCritical, __DynamicallyInvokable]
public static extern void InitializeArray(Array array, RuntimeFieldHandle fldHandle);
所以我们要么需要在已发布的CLR源代码中找到其源代码,但我无法为此方法找到,要么我们可以在程序集级别上进行调试。由于我的Visual-Studio出现问题并且在程序集视图中存在问题,让我们尝试另一种方式,并查看每个数组初始化的内存写入。
从循环初始化开始,在开头我们可以看到有一个空的int[]被初始化(在图片中,0x724a3c88是int[]类型,0x00000004是数组的大小,然后我们可以看到16个字节的零)。
![Empty Array Memory](https://istack.dev59.com/9aINO.webp)
当数组被初始化时,我们可以看到内存中填充了相同的
类型和
大小指示器,只是它还包含数字0到3:
![Initialized Array Memory](https://istack.dev59.com/6tZrx.webp)
当循环迭代时,我们可以看到下一个数组(用红色标记)在第一个数组之后分配,这也意味着每个数组消耗
16 + type + size + padding = 19字节
。
![New Array](https://istack.dev59.com/CKEjH.webp)
对于内联类型初始化器执行相同的过程后,我们可以看到在数组初始化之后,堆中还包含了其他类型,这可能是由于在System.Runtime.CompilerServices.InitializeArray
方法内部进行的,因为数组指针和编译时类型标记被加载到评估堆栈上而不是堆中(在IL代码的L_001B
和L_0020
行):
![Inline Array Initialization](https://istack.dev59.com/oMZO8.webp)
现在使用内联数组初始化程序分配下一个数组,我们可以看到下一个数组仅在第一个数组的开头后64个字节处分配!
![2 Inline Initialized Arrays](https://istack.dev59.com/A9dSD.webp)
因此,内联数组初始化器之所以更慢是由于以下几个原因:
- 分配了更多的内存(来自CLR内部的不需要的内存)。
- 除了数组构造函数之外,还有方法调用开销。
- 如果CLR分配了除数组之外的更多内存,则可能会执行一些其他不必要的操作。
现在来看内联数组初始化器在Debug和Release之间的区别:
如果您检查调试版本的汇编代码,它看起来像这样:
00952E46 B9 42 5D FF 71 mov ecx,71FF5D42h //The pointer to the array.
00952E4B BA 04 00 00 00 mov edx,4 //The desired size of the array.
00952E50 E8 D7 03 F7 FF call 008C322C //Array constructor.
00952E55 89 45 90 mov dword ptr [ebp-70h],eax //The result array (here the memory is an empty array but arr cannot be viewed in the debug yet).
00952E58 B9 E4 0E D7 00 mov ecx,0D70EE4h //The token of the compilation-time-type.
00952E5D E8 43 EF FE 72 call 73941DA5 //First I thought that's the System.Runtime.CompilerServices.InitializeArray method but thats the part where the junk memory is added so i guess it's a part of the token loading process for the compilation-time-type.
00952E62 89 45 8C mov dword ptr [ebp-74h],eax
00952E65 8D 45 8C lea eax,[ebp-74h]
00952E68 FF 30 push dword ptr [eax]
00952E6A 8B 4D 90 mov ecx,dword ptr [ebp-70h]
00952E6D E8 81 ED FE 72 call 73941BF3 //System.Runtime.CompilerServices.InitializeArray method.
00952E72 8B 45 90 mov eax,dword ptr [ebp-70h] //Here the result array is complete
00952E75 89 45 B4 mov dword ptr [ebp-4Ch],eax
另一方面,发布版本的代码如下:
003A2DEF B9 42 5D FF 71 mov ecx,71FF5D42h //The pointer to the array.
003A2DF4 BA 04 00 00 00 mov edx,4 //The desired size of the array.
003A2DF9 E8 2E 04 F6 FF call 0030322C //Array constructor.
003A2DFE 83 C0 08 add eax,8
003A2E01 8B F8 mov edi,eax
003A2E03 BE 5C 29 8C 00 mov esi,8C295Ch
003A2E08 F3 0F 7E 06 movq xmm0,mmword ptr [esi]
003A2E0C 66 0F D6 07 movq mmword ptr [edi],xmm0
003A2E10 F3 0F 7E 46 08 movq xmm0,mmword ptr [esi+8]
003A2E15 66 0F D6 47 08 movq mmword ptr [edi+8],xmm0
调试优化使得无法查看arr的内存,因为在IL级别上该局部变量从未被设置。正如您所看到的,此版本使用movq,这是复制编译时类型的内存到已初始化数组的最快方法,通过两次复制QWORD(2个int放在一起!)来精确地复制我们数组的内容,即16位。