stackoverflow做拳击在C#

我在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 ); } } 

他们都这样做:

  1. 创build一个堆栈(第一个例子是<int>中的generics,第二个是一个对象堆栈)。

  2. 声明一个recursion调用n次(n> = 0)的方法,并且在每一步中,在创build的栈中推入1000个整数。

当我用Foo(30000)运行第一个例子时,没有发生exception,但第二个例子与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指令并不是衡量“工作”的准确方法。

也意识到CIL不是被执行的东西。 JIT将CIL编译为实际的机器指令,并进行优化阶段,所以CIL可能与实际执行的指令大不相同。

在第二种情况下,由于您使用的是非generics集合,因此每个Push调用都需要将整数加框,就像在CIL中确定的那样。

拳击一个整数有效地创build一个对象“包装”你的Int32 。 现在,不需要将一个32位整数加载到堆栈上,而是必须将一个32位整数加载到堆栈上,然后将其加载,这样也可以将对象引用加载到堆栈上。

如果您在“反汇编”窗口中检查了这一点,则可以看到generics和非generics版本之间的差异是显着的,并且比生成的CIL所build议的要重要得多。

通用版本有效地编译为一系列的调用,如下所示:

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

另一方面,非generics必须创build盒装对象,而是编译为:

 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位)的调用都会产生一个stackoverflow,这意味着整数和盒装对象引用(每个4字节)都被保留在堆栈上,如127 * 1000 * 8 == 1016000,这对于.NET应用程序来说是非常接近默认的1 MB线程堆栈大小。

当使用generics版本时,由于没有装箱对象,所以整数不必全部存储在堆栈上,并且相同的寄存器被重用。 这可以使您在使用堆栈之前显着提升(在我的系统上大于40000)。

请注意,这将是CLR版本和平台相关的,因为在x86 / x64上也有不同的JIT。