DLe*_*Leh 35 .net c# arrays performance initialization
为什么内联数组初始化比迭代这样慢得多?我运行这个程序来比较它们,单个初始化比使用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();
}
Run Code Online (Sandbox Code Playgroud)
结果:
Array - Single Init
00:00:26.9590931
Array - Iterative
00:00:02.0345341
Run Code Online (Sandbox Code Playgroud)
我还在VS2013社区版上运行了这个版本,并在另一台PC上运行了最新的VS2015预览,并得到了与我的LinqPad结果类似的结果.
我在Release模式下运行代码(即:编译器优化),并从上面得到非常不同的结果.这次两个代码块非常相似.这似乎表明它是编译器优化问题.
Array - Single Init
00:00:00.5511516
Array - Iterative
00:00:00.5882975
Run Code Online (Sandbox Code Playgroud)
Tam*_*red 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))
Run Code Online (Sandbox Code Playgroud)
第二行调用一个方法,该方法使用我们刚刚创建的空数组返回初始化数组,L_0060并使用此Compile-Time-Type.
如果我们尝试查看此方法的代码,我们将看到它是在CLR中实现的:
[MethodImpl(MethodImplOptions.InternalCall), SecuritySafeCritical, __DynamicallyInvokable]
public static extern void InitializeArray(Array array, RuntimeFieldHandle fldHandle);
Run Code Online (Sandbox Code Playgroud)
所以要么我们需要在已发布的CLR源代码中找到它的源代码,我找不到这个方法,或者我们可以在汇编级别进行调试.由于我现在遇到我的Visual-Studio问题并且在组装视图中遇到问题,让我们尝试另一种态度并查看每个阵列初始化的内存写入.
从循环初始化开始,在开始时我们可以看到有空的int[]初始化(在Little-Endian中0x724a3c88看到的图片是类型,并且是数组的大小,而不是我们可以看到16个字节的零).int[]0x00000004
当数组初始化时,我们可以看到内存中填充了相同的类型和大小指示符,只有它还有数字0到3:
当循环迭代时,我们可以看到它在我们的第一个数组(未签名)之后分配的下一个数组(用红色签名),这也意味着每个数组都消耗16 + type + size + padding = 19 bytes:
在内联类型初始化器上执行相同的过程,我们可以看到在初始化数组之后,堆包含除数组之外的其他类型 ; 这可能来自System.Runtime.CompilerServices.InitializeArray方法内部,因为数组指针和编译时类型令牌被加载到评估堆栈而不是堆上(行L_001B和L_0020IL代码中):
现在使用内联数组初始化程序分配下一个数组,向我们显示下一个数组在第一个数组开始后只分配了64个字节!
因此, 内联阵列初始化 器的速度最慢,因为几个原因:
现在,之间的差额调试和发布的内嵌数组初始化:
如果检查调试版本的汇编代码,它看起来像这样:
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
Run Code Online (Sandbox Code Playgroud)
另一方面,发布版本的代码如下所示:
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
Run Code Online (Sandbox Code Playgroud)
调试优化使得无法查看arr的内存,因为从未设置IL级别的本地.你可以看到这个版本正在使用movq哪个是通过复制2次a (2 s在一起!)将编译时间类型的内存复制到初始化数组的最快方法,这是我们数组的内容是的.QWORDint16 bit
静态数组初始化以不同的方式实现.它会将程序集中的位存储为嵌入式类,其名称类似于<PrivateImplementationDetails>....
它的作用是将数组数据作为位在一个特殊位置的程序集中存储; 然后将从程序集加载它,它将调用RuntimeHelpers.InitializeArray初始化数组.
请注意,如果您使用反射器来查看已编译的源,因为C#您不会注意到我在这里描述的任何内容.您需要IL查看反射器或任何此类反编译工具中的视图.
[MethodImpl(MethodImplOptions.InternalCall), SecuritySafeCritical, __DynamicallyInvokable]
public static extern void InitializeArray(Array array, RuntimeFieldHandle fldHandle);
Run Code Online (Sandbox Code Playgroud)
您可以看到这是在CLR(标记为InternalCall)中实现的,然后映射到COMArrayInfo::InitializeArray(sscli中的ecall.cpp).
FCIntrinsic("InitializeArray", COMArrayInfo::InitializeArray, CORINFO_INTRINSIC_InitializeArray)
Run Code Online (Sandbox Code Playgroud)
COMArrayInfo::InitializeArray(生活在comarrayinfo.cpp中)是一种神奇的方法,它使用嵌入在程序集中的位来初始化数组.
我不确定为什么要花很多时间才能完成; 我对此没有很好的解释.我想这是因为它从物理组件中提取并提取数据?我不确定.你可以自己深入研究这些方法.但是你可以知道它没有被编译成你在代码中看到的那样.
你可以使用像这样的工具IlDasm,并Dumpbin找到更多相关信息,当然也可以下载sscli.
FWIW:我从Pluralsight"bart de smet"的课程中得到了这些信息
| 归档时间: |
|
| 查看次数: |
1995 次 |
| 最近记录: |