【问题标题】:Why is creating an array with inline initialization so slow?为什么使用内联初始化创建数组这么慢?
【发布时间】:2015-01-27 21:51:04
【问题描述】:

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

【问题讨论】:

  • 您的迭代列表添加了 10 个项目,其他的添加了 4 个。
  • 您的测试可能无效,因为编译器可以优化死代码,而您没有对循环中创建的对象做任何事情。
  • @PiotrZierhoffer 显然,如果您禁用优化,代码将被严重优化。为什么这会让任何人感到惊讶?如果你想关心操作的时间,你需要优化,否则结果是没有意义的。
  • 有没有人意识到内联版本创建了一个 {1,2,3,4} 数组,而迭代版本创建了一个 {0,1,2,3} 数组?
  • @displayName 哈哈是的,但这不影响性能:)

标签: c# .net arrays performance initialization


【解决方案1】:

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

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

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

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

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

    • 前 2 行正在创建一个大小为 4 的数组。
    • 第三行将生成的数组的指针复制到计算堆栈上。
    • 最后一行是刚刚创建的数组的本地数组。

现在我们将关注剩下的 2 行:

第一行 (L_001B) 加载了一些 Compilation-Time-Type,其类型名称为 __StaticArrayInitTypeSize=16,其字段名称为 1456763F890A84558F99AFA687C36B9037697848,它位于名为 &lt;PrivateImplementationDetails&gt; 的类中在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 现在有问题,并且它的程序集视图也有问题,让我们尝试另一种态度,查看每个数组初始化的内存写入

从循环初始化开始,一开始我们可以看到有en empty int[] initialized(图中Little-Endian中看到的0x724a3c88int[]的类型,0x00000004是大小数组,我们可以看到 16 个字节的零)。

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

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

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

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

所以 inline-array-initializer 至少会因为几个原因而变慢:: p>

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

现在了解内联数组初始化器DebugRelease之间的区别:

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

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

【讨论】:

  • 非常令人印象深刻的答案,您是 MSIL 的大师!谢谢
  • 我很确定那是 little-endian,而不是 big-endian。 Big-endian 意味着最重要的字节在前。这些是小端:0x00000004 存储为04 00 00 000x724a3c88 存储为88 3c 4a 72,最低有效字节(小端)在前。
  • 我时不时会停下来回答这个问题,以尝试每次都多掌握一点
  • InitializeArray 的来源在这里:github.com/dotnet/coreclr/blob/…
【解决方案2】:

静态数组初始化的实现方式略有不同。它将位存储在程序集中作为嵌入式类,该类将被命名为&lt;PrivateImplementationDetails&gt;...

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

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

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

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

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

COMArrayInfo::InitializeArray(位于 comarrayinfo.cpp 中)是一种神奇的方法,它使用嵌入在程序集中的位中的值来初始化数组。

我不知道为什么这需要很长时间才能完成;我对此没有很好的解释。我想这是因为它会从物理组件中提取数据?我不知道。您可以自己挖掘方法。 但是你可以知道它不会被编译成你在代码中看到的那样。

您可以使用IlDasmDumpbin 之类的工具来查找更多相关信息,当然还可以下载sscli

FWIW:我从 Pluralsight course by "bart de smet" 获得了这些信息

【讨论】:

  • 你在谈论实际上并没有被使用的代码。抖动优化器将其删除,并且根本没有留下任何数组。该问题的基本问题是基准不代表真实的代码,并且没有在启用优化器的情况下执行。
  • @HansPassant 是的,我注意到当优化开关打开时它运行得非常快(显然得到了优化)。当优化关闭时,也许这种魔法会导致性能下降?
猜你喜欢
  • 1970-01-01
  • 2016-09-28
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-06-02
  • 1970-01-01
相关资源
最近更新 更多