为什么使用内联初始化创建数组这么慢?
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 Edition 和 latest 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::InitializeArray
(ecall.cpp 在 sscli).
FCIntrinsic("InitializeArray", COMArrayInfo::InitializeArray, CORINFO_INTRINSIC_InitializeArray)
COMArrayInfo::InitializeArray
(存在于comarrayinfo.cpp)是用值初始化数组的神奇方法来自嵌入程序集的位。
我不确定为什么要花很多时间才能完成;我对此没有很好的解释。我猜这是因为它从物理组件中提取数据?我不确定。您可以自己深入研究这些方法。
但是您可以了解到它不会像您在代码中看到的那样被编译。
您可以使用 IlDasm
和 Dumpbin
等工具找到更多相关信息,当然还可以下载 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 个字节的零)。
数组初始化时我们可以看到内存中填充了相同的type和size指标,只是它还有其中的数字 0 到 3:
当循环迭代时,我们可以看到它在我们的第一个数组(未签名)之后分配了下一个数组(以红色标记),这也意味着每个数组消耗 16 + type + size + padding = 19 bytes
:
在inline-type-initializer上做同样的处理我们可以看到在数组被初始化之后,堆中还包含其他类型 除了我们的数组;这可能来自 System.Runtime.CompilerServices.InitializeArray
方法,因为数组指针和 compile-time-type 标记被加载到计算堆栈上而不是堆上(行 L_001B
和 IL 代码中的 L_0020
):
现在使用 内联数组初始值设定项分配下一个数组 向我们展示 下一个数组仅在第一个数组开始后的 64 字节处分配!
所以 inline-array-initializer 在最小值,原因不多:
- 分配了更多的内存(CLR 中不需要的内存)。
- 除了数组构造函数之外,还有一个方法调用开销。
- 此外,如果 CLR 分配了比数组更多的内存 - 它可能会执行更多不必要的操作。
现在了解 Debug 和 Release 在 inline 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
.
为什么内联数组初始化比迭代初始化慢得多?我 运行 这个程序来比较它们,单个初始化花费的时间比 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 Edition 和 latest 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::InitializeArray
(ecall.cpp 在 sscli).
FCIntrinsic("InitializeArray", COMArrayInfo::InitializeArray, CORINFO_INTRINSIC_InitializeArray)
COMArrayInfo::InitializeArray
(存在于comarrayinfo.cpp)是用值初始化数组的神奇方法来自嵌入程序集的位。
我不确定为什么要花很多时间才能完成;我对此没有很好的解释。我猜这是因为它从物理组件中提取数据?我不确定。您可以自己深入研究这些方法。 但是您可以了解到它不会像您在代码中看到的那样被编译。
您可以使用 IlDasm
和 Dumpbin
等工具找到更多相关信息,当然还可以下载 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 个字节的零)。
数组初始化时我们可以看到内存中填充了相同的type和size指标,只是它还有其中的数字 0 到 3:
当循环迭代时,我们可以看到它在我们的第一个数组(未签名)之后分配了下一个数组(以红色标记),这也意味着每个数组消耗 16 + type + size + padding = 19 bytes
:
在inline-type-initializer上做同样的处理我们可以看到在数组被初始化之后,堆中还包含其他类型 除了我们的数组;这可能来自 System.Runtime.CompilerServices.InitializeArray
方法,因为数组指针和 compile-time-type 标记被加载到计算堆栈上而不是堆上(行 L_001B
和 IL 代码中的 L_0020
):
现在使用 内联数组初始值设定项分配下一个数组 向我们展示 下一个数组仅在第一个数组开始后的 64 字节处分配!
所以 inline-array-initializer 在最小值,原因不多:
- 分配了更多的内存(CLR 中不需要的内存)。
- 除了数组构造函数之外,还有一个方法调用开销。
- 此外,如果 CLR 分配了比数组更多的内存 - 它可能会执行更多不必要的操作。
现在了解 Debug 和 Release 在 inline 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
.