簡體   English   中英

Stackoverflow在C#做拳擊

[英]Stackoverflow doing boxing in 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. 創建一個堆棧(第一個示例的<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

第二

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可能與實際執行的指令非常不同。

在第二種情況下,由於您使用的是非泛型集合,因此每次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。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM