Stackoverflow在C#做拳击

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)

他们都这样做:

  1. 创建一个堆栈(<int>第一个示例为通用,第二个示例为对象堆栈).

  2. 声明一个递归调用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.

  • 当我测试这个问题时,我使用了一个for循环 - `for(int i = 0; i <= 999; i ++)S.Push(i);` - 并且得到的行为类似于OP.如果编译器正在执行循环展开,这只适合您的解释.你认为是这种情况吗? (2认同)
  • JIT是否有充分理由将引用留在堆栈中? (2认同)