我在C#中有以下两个代码块:
第一的
class Program
{
static Stack<int> S = new Stack<int>();
static int Foo(int n) {
if (n == 0)
return 0;
S.Push(0);
S.Push(1);
...
S.Push(999);
return Foo( n-1 );
}
}
第二
class Program
{
static Stack S = new Stack();
static int Foo(int n) {
if (n == 0)
return 0;
S.Push(0);
S.Push(1);
...
S.Push(999);
return Foo( n-1 );
}
}
他们都做同样的事情:
<int>
中是通用的,对于第二个示例,则是对象的堆栈)。 当我使用
Foo(30000)
运行第一个示例时,没有异常发生,但是第二个示例使用Foo(1000)
崩溃,仅n = 1000。当我看到两种情况下生成的CIL时,唯一的区别是每次推送的装箱部分:
第一的
IL_0030: ldsfld class [System]System.Collections.Generic.Stack`1<int32> Test.Program::S
IL_0035: ldc.i4 0x3e7
IL_003a: callvirt instance void class [System]System.Collections.Generic.Stack`1<int32>::Push(!0)
IL_003f: nop
第二
IL_003a: ldsfld class [mscorlib]System.Collections.Stack Test.Program::S
IL_003f: ldc.i4 0x3e7
IL_0044: box [mscorlib]System.Int32
IL_0049: callvirt instance void [mscorlib]System.Collections.Stack::Push(object)
IL_004e: nop
我的问题是:为什么,如果第二个示例的CIL堆栈没有明显的过载,它的崩溃速度是否比第一个更快?
最佳答案
请注意,CIL指令的数量不能准确表示将要使用的工作量或内存量。一条指令的影响可能很小,也可能影响很大,因此对CIL指令进行计数不是衡量“工作”的准确方法。
还应意识到,CIL不是要执行的内容。 JIT在优化阶段将CIL编译为实际的机器指令,因此CIL可能与实际执行的指令有很大不同。
在第二种情况下,由于您使用的是非泛型集合,所以每个Push
调用都需要将整数装箱,就像在CIL中确定的那样。
将整数装箱有效地创建了一个为您“包装” Int32
的对象。现在,它不仅需要将32位整数加载到堆栈上,还必须将32位整数加载到堆栈上,然后将其装箱,这实际上也将对象引用加载到堆栈上。
如果在“反汇编”窗口中对此进行检查,则可以看到通用版本与非通用版本之间的差异是巨大的,并且比生成的CIL所建议的要重要得多。
通用版本可以有效地编译为一系列调用,如下所示:
0000022c nop
S.Push(25);
0000022d mov ecx,dword ptr ds:[03834978h]
00000233 mov edx,19h
00000238 cmp dword ptr [ecx],ecx
0000023a call 71618DD0
0000023f nop
S.Push(26);
00000240 mov ecx,dword ptr ds:[03834978h]
00000246 mov edx,1Ah
0000024b cmp dword ptr [ecx],ecx
0000024d call 71618DD0
00000252 nop
S.Push(27);
另一方面,非泛型必须创建装箱的对象,然后编译为:
00000645 nop
S.Push(25);
00000646 mov ecx,7326560Ch
0000064b call FAAC20B0
00000650 mov dword ptr [ebp-48h],eax
00000653 mov eax,dword ptr ds:[03AF4978h]
00000658 mov dword ptr [ebp+FFFFFEE8h],eax
0000065e mov eax,dword ptr [ebp-48h]
00000661 mov dword ptr [eax+4],19h
00000668 mov eax,dword ptr [ebp-48h]
0000066b mov dword ptr [ebp+FFFFFEE4h],eax
00000671 mov ecx,dword ptr [ebp+FFFFFEE8h]
00000677 mov edx,dword ptr [ebp+FFFFFEE4h]
0000067d mov eax,dword ptr [ecx]
0000067f mov eax,dword ptr [eax+2Ch]
00000682 call dword ptr [eax+18h]
00000685 nop
S.Push(26);
00000686 mov ecx,7326560Ch
0000068b call FAAC20B0
00000690 mov dword ptr [ebp-48h],eax
00000693 mov eax,dword ptr ds:[03AF4978h]
00000698 mov dword ptr [ebp+FFFFFEE0h],eax
0000069e mov eax,dword ptr [ebp-48h]
000006a1 mov dword ptr [eax+4],1Ah
000006a8 mov eax,dword ptr [ebp-48h]
000006ab mov dword ptr [ebp+FFFFFEDCh],eax
000006b1 mov ecx,dword ptr [ebp+FFFFFEE0h]
000006b7 mov edx,dword ptr [ebp+FFFFFEDCh]
000006bd mov eax,dword ptr [ecx]
000006bf mov eax,dword ptr [eax+2Ch]
000006c2 call dword ptr [eax+18h]
000006c5 nop
在这里,您可以看到拳击的重要性。
在您的情况下,将整数装箱会导致将装箱的对象引用加载到堆栈上。在我的系统上,这会导致大于
Foo(127)
(32位)的任何调用上的堆栈溢出,这表明整数和装箱的对象引用(每个4字节)都保留在堆栈上,因为127 * 1000 * 8 = = 1016000,危险地接近.NET应用程序的默认1 MB线程堆栈大小。使用通用版本时,由于没有装箱的对象,因此整数不必全部存储在堆栈中,并且可以重复使用同一寄存器。这使您在用完堆栈之前可以进行更多的递归操作(在我的系统上> 40000)。
请注意,这将取决于CLR版本和平台,因为x86/x64上的JIT也不同。
关于c# - Stackoverflow在C#中进行拳击,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/28865139/