为什么使用内联初始化创建数组这么慢?

Why is creating an array with inline initialization so slow?

为什么内联数组初始化比迭代初始化慢得多?我 运行 这个程序来比较它们,单个初始化花费的时间比 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

我也在 VS2013 Community Editionlatest VS2015 preview 上 运行 在另一台 PC 上进行了此操作,并得到了相似的结果我的 LinqPad 结果。

I 运行 Release 模式下的代码(即:编译器优化开启),得到与上面截然不同的结果。这次这两个代码块非常相似。这似乎表明这是一个编译器优化问题。

Array - Single Init
00:00:00.5511516

Array - Iterative
00:00:00.5882975

静态数组初始化的实现方式略有不同。它会将程序集中的位存储为嵌入式 class ,其名称类似于 <PrivateImplementationDetails>....

它的作用是将数组数据作为位存储在程序集中的某个特殊位置;然后将从程序集中加载它,它将调用 RuntimeHelpers.InitializeArray 来初始化数组。

请注意,如果您使用反射器将编译后的源代码查看为 C#,您将不会注意到我在这里描述的任何内容。您需要查看反射器或任何此类反编译工具中的 IL 视图。

[MethodImpl(MethodImplOptions.InternalCall), SecuritySafeCritical, __DynamicallyInvokable]
public static extern void InitializeArray(Array array, RuntimeFieldHandle fldHandle);

你可以看到这是在 CLR 中实现的(标记为 InternalCall),然后映射到 COMArrayInfo::InitializeArrayecall.cppsscli).

FCIntrinsic("InitializeArray", COMArrayInfo::InitializeArray, CORINFO_INTRINSIC_InitializeArray)

COMArrayInfo::InitializeArray(存在于comarrayinfo.cpp)是用值初始化数组的神奇方法来自嵌入程序集的位。

我不确定为什么要花很多时间才能完成;我对此没有很好的解释。我猜这是因为它从物理组件中提取数据?我不确定。您可以自己深入研究这些方法。 但是您可以了解到它不会像您在代码中看到的那样被编译。

您可以使用 IlDasmDumpbin 等工具找到更多相关信息,当然还可以下载 sscli.

FWIW:我从 "bart de smet"

Pluralsight 课程中获得了这些信息

首先,C# 级别的分析不会给我们任何东西 因为它会向我们显示执行时间最长的 C# 代码行,这当然是内联数组初始化,但对于这项运动:

现在,当我们看到预期的结果时,让我们观察 IL 级别的代码 并尝试查看 2 个数组的初始化之间有什么不同:

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

    一切看起来都很好,循环完全按照我们的预期进行,没有明显的开销。

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

    • 前两行创建一个大小为 4 的数组。
    • 第三行将生成的数组指针复制到计算堆栈上。
    • 最后一行将数组局部设置为刚刚创建的数组。

现在我们将重点放在剩下的 2 行上:

第一行(L_001B)加载了一些Compilation-Time-Type,其类型名称为__StaticArrayInitTypeSize=16,其字段名称为1456763F890A84558F99AFA687C36B9037697848 并且它位于 Root Namespace 中名为 <PrivateImplementationDetails> 的 class 中。如果我们查看此字段,我们会发现它完全包含所需的数组,正如我们希望将其编码为字节:

.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))

第二行,调用一个方法,该方法returns 使用我们刚刚在 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 时遇到问题,而且它的程序集视图也有问题,让我们尝试另一种态度并 看看内存写入 对于每个数组初始化。

从循环初始化开始,一开始我们可以看到初始化了一个空的int[](图中0x724a3c88看到Little-Endian就是[=20的类型=] 和 0x00000004 是数组的大小,比我们可以看到 16 个字节的零)。

数组初始化时我们可以看到内存中填充了相同的typesize指标,只是它还有其中的数字 0 到 3:

当循环迭代时,我们可以看到它在我们的第一个数组(未签名)之后分配了下一个数组(以红色标记),这也意味着每个数组消耗 16 + type + size + padding = 19 bytes:

inline-type-in​​itializer上做同样的处理我们可以看到在数组被初始化之后,堆中还包含其他类型 除了我们的数组;这可能来自 System.Runtime.CompilerServices.InitializeArray 方法,因为数组指针和 compile-time-type 标记被加载到计算堆栈上而不是堆上(行 L_001B 和 IL 代码中的 L_0020):

现在使用 内联数组初始值设定项分配下一个数组 向我们展示 下一个数组仅在第一个数组开始后的 64 字节处分配!

所以 inline-array-initializer 在最小值,原因不多:

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

现在了解 DebugReleaseinline array initializer 中的区别:

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

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,这是通过复制 2 次 将编译时类型的内存 复制到初始化数组的最快方法a QWORD(2 int 在一起!)这正是我们数组的内容,即 16 bit.