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

时间:2015-01-27 21:51:04

标签: c# .net 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();
}

结果:

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

2 个答案:

答案 0 :(得分:47)

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

Profiling Results

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

  • 首先,我们将看一下标准数组初始化

    For Loop

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

  • 现在让我们来看看内联数组初始化

    Inline Array Initializer

    • 前两行正在创建一个大小为4的数组。
    • 第三行将生成的数组指针复制到评估堆栈上。
    • 最后一行是刚刚创建的数组的array-local。

现在我们将重点关注其余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个字节的零。

Little-Endian

当数组初始化时,我们可以看到内存中填充了相同的类型 size 指示符,只有它还包含数字0到3:

Empty Array Memory

当循环迭代时,我们可以看到它在我们的第一个数组(未签名)之后分配的下一个数组(用红色签名),这也意味着每个数组都消耗L_001B

Initialized Array Memory

inline-type-in​​itializer 上执行相同的过程,我们可以看到,在初始化数组之后,堆包含除数组之外的其他类型;这可能来自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

New Array

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

Inline Array Initialization

所以 内联阵列初始化程序 由于几个原因而至少变慢

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

现在了解内联数组初始化程序中的 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 )是一种神奇的方法,它使用汇编中嵌入的位值来初始化数组。

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

您可以使用IlDasmDumpbin等工具查找更多相关信息,当然也可以下载 sscli 。< / p>

FWIW:我已经通过&#34; bart de smet&#34;

Pluralsight课程获得了这些信息