lca*_*lov 49 c# stack-overflow il
我在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 );
}
}
Run Code Online (Sandbox Code Playgroud)
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 );
}
}
Run Code Online (Sandbox Code Playgroud)
他们都这样做:
创建一个堆栈(<int>第一个示例为通用,第二个示例为对象堆栈).
声明一个递归调用n次(n> = 0)的方法,并在每个步骤中在创建的堆栈内推送1000个整数.
当我运行第一个例子时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
Run Code Online (Sandbox Code Playgroud)
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
Run Code Online (Sandbox Code Playgroud)
我的问题是:为什么,如果第二个例子的CIL堆栈没有明显的重载,它是否比第一个例子"更快"崩溃?
Ree*_*sey 59
为什么,如果第二个例子的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);
Run Code Online (Sandbox Code Playgroud)
另一方面,非泛型必须创建盒装对象,而是编译为:
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
Run Code Online (Sandbox Code Playgroud)
在这里你可以看到拳击的意义.
在您的情况下,装箱整数会导致装箱对象引用加载到堆栈中.在我的系统上,这会导致任何大于Foo(127)(32位)的调用的堆栈溢出,这表明整数和盒装对象引用(每个4个字节)都保存在堆栈中,如127*1000*8 == 1016000,危险地接近.NET应用程序的默认1 MB线程堆栈大小.
使用通用版本时,由于没有盒装对象,因此整数不必全部存储在堆栈中,并且正在重用相同的寄存器.这使您可以在用完堆栈之前显着增加(在我的系统上> 40000).
请注意,这将取决于CLR版本和平台,因为x86/x64上还有不同的JIT.