为什么内联数组初始化比迭代这样慢得多?我运行这个程序来比较它们,单个初始化比使用for
循环这样做要花费很多倍。
这是我在LinqPad
中编写的程序来测试它。
var iterations = 100000000;
var length = 4;
{
var timer = System.Diagnostics.Stopwatch.StartNew();
for(int i = 0; i < iterations; i++){
var arr = new int[] { 1, 2, 3, 4 };
}
timer.Stop();
"Array- Single Init".Dump();
timer.Elapsed.Dump();
}
{
var timer = System.Diagnostics.Stopwatch.StartNew();
for(int i = 0; i < iterations; i++){
var arr = new int[length];
for(int j = 0; j < length; j++){
arr[j] = j;
}
}
timer.Stop();
"Array- Iterative".Dump();
timer.Elapsed.Dump();
}
结果:
Array - Single Init
00:00:26.9590931
Array - Iterative
00:00:02.0345341
我还在另一台PC上的 VS2013社区版和最新的VS2015预览上运行此功能,并获得与我的LinqPad
结果类似的结果。
我以Release
模式运行代码(即:编译器优化),并从上面得到了非常不同的结果。这次两个代码块非常相似。这似乎表明它是编译器优化问题。
Array - Single Init
00:00:00.5511516
Array - Iterative
00:00:00.5882975
答案 0 :(得分:47)
首先,在C#级别进行分析将不会给我们什么,因为它会向我们展示执行时间最长的C#代码行,这当然是内联数组初始化,但对于这项运动:
现在,当我们看到预期结果时,让观察IL级别的代码,并尝试查看2个数组的初始化之间有什么不同:
首先,我们将看一下标准数组初始化:
一切看起来都不错,循环正在完全按照我们的预期进行而没有明显的开销。
现在让我们来看看内联数组初始化:
现在我们将重点关注其余2行:
第一行(L_001B
)加载一些 Compilation-Time-Type ,其类型名称为__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
中创建的空数组并使用此 Compile-Time-Type 返回初始化数组。
如果我们尝试查看此方法的代码,我们会看到它是implemented within the CLR:
[MethodImpl(MethodImplOptions.InternalCall), SecuritySafeCritical, __DynamicallyInvokable]
public static extern void InitializeArray(Array array, RuntimeFieldHandle fldHandle);
因此,我们需要在已发布的CLR源中找到它的源代码,这是我找不到的方法,或者我们可以在汇编级别调试。 由于我现在遇到 Visual-Studio 时出现问题并且在汇编视图中出现问题,让我们尝试另一种态度,然后查看每次数组初始化的内存写入。 / p>
从循环初始化开始,我们可以看到初始化为int[]
的{{1}} 0x724a3c88
int[]
中0x00000004
的类型16 + type + size + padding = 19 bytes
并且System.Runtime.CompilerServices.InitializeArray
是数组的大小,而不是我们可以看到16个字节的零。
当数组初始化时,我们可以看到内存中填充了相同的类型和 size 指示符,只有它还包含数字0到3:
当循环迭代时,我们可以看到它在我们的第一个数组(未签名)之后分配的下一个数组(用红色签名),这也意味着每个数组都消耗L_001B
:
在 inline-type-initializer 上执行相同的过程,我们可以看到,在初始化数组之后,堆包含除数组之外的其他类型;这可能来自L_0020
方法,因为数组指针和编译时类型标记被加载到评估堆栈而不是堆上(行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
和IL代码中的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
:
现在使用内联数组初始化程序分配下一个数组,向我们显示下一个数组在第一个数组开始后只分配了64个字节!
所以 内联阵列初始化程序 由于几个原因而至少变慢:
现在了解内联数组初始化程序中的 Debug 和 Release 之间的区别:
如果检查调试版本的汇编代码,它看起来像是:
movq
另一方面,发布版本的代码如下:
QWORD
调试优化使得无法查看arr的内存,因为 IL级别的本地永远不会设置。
正如您所看到的,此版本正在使用int
,这是将编译时间类型的内存复制到初始化数组的最快方法,方法是复制2次{{ 1}}(2 16 bit
s together!)这是我们数组的内容{{1}}。
答案 1 :(得分:6)
静态数组初始化以不同的方式实现。它会将程序集中的位存储为嵌入类,其名称类似于<PrivateImplementationDetails>...
。
它的作用是将数组数据作为位在一个特殊位置的程序集中存储;然后将从程序集中加载它,它将调用RuntimeHelpers.InitializeArray
来初始化数组。
请注意,如果您使用反射器查看已编译的源C#
,您将不会注意到我在此处所描述的任何内容。您需要查看反射器或任何此类反编译工具中的IL
视图。
[MethodImpl(MethodImplOptions.InternalCall), SecuritySafeCritical, __DynamicallyInvokable]
public static extern void InitializeArray(Array array, RuntimeFieldHandle fldHandle);
您可以在CLR
(标记为InternalCall
)中看到这一点,然后映射到COMArrayInfo::InitializeArray
( ecall.cpp 在 sscli )。
FCIntrinsic("InitializeArray", COMArrayInfo::InitializeArray, CORINFO_INTRINSIC_InitializeArray)
COMArrayInfo::InitializeArray
(位于 comarrayinfo.cpp )是一种神奇的方法,它使用汇编中嵌入的位值来初始化数组。
我不确定为什么要花很多时间才能完成;我对此没有很好的解释。我想这是因为它从物理组件中提取并提取数据?我不确定。你可以自己深入研究这些方法。 但是你可以知道它没有被编译成你在代码中看到的那样。
您可以使用IlDasm
和Dumpbin
等工具查找更多相关信息,当然也可以下载 sscli 。< / p>
FWIW:我已经通过&#34; bart de smet&#34;
从Pluralsight
课程获得了这些信息