为什么创建一个内联初始化的数组这么慢?

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个数组的初始化之间的不同之处:

  • 首先我们来看看标准数组初始化:

    对于循环

    一切看起来都不错,循环正在完全按照我们的预期进行,没有明显的开销.

  • 现在让我们看一下内联数组初始化:

    内联阵列初始化器

    • 前两行是创建一个大小为4的数组.
    • 第三行将生成的数组指针复制到评估堆栈上.
    • 最后一行设置是刚创建的数组的本地数组.

现在我们将重点关注剩余的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_001BL_0020IL代码中):

内联数组初始化

现在使用内联数组初始化程序分配下一个数组,向我们显示下一个数组在第一个数组开始后只分配了64个字节!

2内联初始化数组

因此, 内联阵列初始化 器的速度最慢,因为几个原因:

  • 分配了更多的内存(CLR中的不需要的内存).
  • 除了数组构造函数之外,还有一个方法调用开销.
  • 此外,如果CLR分配了除阵列之外的更多内存 - 它可能会执行一些更不必要的操作.

现在,之间的差额调试发布内嵌数组初始化:

如果检查调试版本的汇编代码,它看起来像这样:

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

  • 非常详细的分析!感谢您的见解! (4认同)

Sri*_*vel 6

静态数组初始化以不同的方式实现.它会将程序集中的位存储为嵌入式类,其名称类似于<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"的课程中得到了这些信息

  • 你在谈论实际上没有被使用的代码.抖动优化器将其删除,并且根本没有数组.该问题的基本问题是基准测试不代表实际代码,并且在启用优化程序时不执行. (5认同)