簡體   English   中英

C#編譯器如何優化代碼片段?

[英]How does the C# compiler optimize a code fragment?

如果我有這樣的代碼

for(int i=0;i<10;i++)
{
    int iTemp;
    iTemp = i;
    //.........
}

編譯器是否會立即對iTemp進行10次操作?

或者它優化它?

我的意思是如果我重寫循環為

int iTemp;
for(int i=0;i<10;i++)
{
    iTemp = i;
    //.........
}

會更快嗎?

使用反射器可以查看C#編譯器生成的IL。

.method private hidebysig static void Way1() cil managed
{
    .maxstack 2
    .locals init (
        [0] int32 i)
    L_0000: ldc.i4.0 
    L_0001: stloc.0 
    L_0002: br.s L_0008
    L_0004: ldloc.0 
    L_0005: ldc.i4.1 
    L_0006: add 
    L_0007: stloc.0 
    L_0008: ldloc.0 
    L_0009: ldc.i4.s 10
    L_000b: blt.s L_0004
    L_000d: ret 
}

.method private hidebysig static void Way2() cil managed
{
    .maxstack 2
    .locals init (
        [0] int32 i)
    L_0000: ldc.i4.0 
    L_0001: stloc.0 
    L_0002: br.s L_0008
    L_0004: ldloc.0 
    L_0005: ldc.i4.1 
    L_0006: add 
    L_0007: stloc.0 
    L_0008: ldloc.0 
    L_0009: ldc.i4.s 10
    L_000b: blt.s L_0004
    L_000d: ret 
}

它們完全相同,因此在聲明iTemp時不會產生任何性能差異。

正如其他人所說,你所展示的代碼產生等效的IL,除非變量被lambda表達式捕獲以供稍后執行。 在這種情況下,代碼是不同的,因為它必須跟蹤表達式的變量的當前值。 可能還有其他情況也沒有進行優化。

當您想要捕獲lambda表達式的值時,創建循環變量的新副本是一種常用技術。

嘗試:

var a = new List<int> { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };

var q = a.AsEnumerable();
int iTemp;
for(int i=0;i<10;i++) 
{ 
    iTemp = i;
    q = q.Where( x => x <= iTemp );
}

Console.WriteLine(string.Format( "{0}, count is {1}",
    string.Join( ":", q.Select( x => x.ToString() ).ToArray() ),
    q.Count() ) );

var a = new List<int> { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };

var q = a.AsEnumerable();
for(int i=0;i<10;i++) 
{ 
    var iTemp = i;
    q = q.Where( x => x <= iTemp );
}

Console.WriteLine(string.Format( "{0}, count is {1}",
    string.Join( ":", q.Select( x => x.ToString() ).ToArray() ),
    q.Count() ) );

如果你真的很好奇CSC(C#編譯器)如何處理你的代碼,你可能想要使用LINQPad - 它允許你輸入簡短的C#表達式或程序,並查看生成的IL( CLR字節碼)。

要記住的一件事是局部變量通常在堆棧上分配。 編譯器必須完成的一項任務是確定特定方法需要多少堆棧空間並將其置於一邊。

考慮:

int Func(int a, int b, int c)
{
    int x = a * 2;
    int y = b * 3;
    int z = c * 4;
    return x + y + z;
 }

忽略這一事實可以很容易地優化為返回(a * 2)+(b * 3)+(c * 4),編譯器將看到三個局部變量並為三個局部變量留出空間。

如果我有這個:

int Func(int a, int b, int c)
{
    int x = a * 2;
    {
        int y = b * 3;
        {
            int z = c * 4;
            {
                return x + y + z;
            }
        }
     }
 }

它仍然是相同的3個局部變量 - 只是在不同的范圍內。 for循環只是一個帶有一些膠水代碼的示波器塊,可以使它工作。

現在考慮一下:

int Func(int a, int b, int c)
{
    int x = a * 2;
    {
        int y = b * 3;
        x += y;
    }
    {
        int z = c * 4;
        x += z;
    }
    return x;
}

這是唯一可能不同的情況。 你有變量y和z進出范圍 - 一旦它們超出范圍,就不再需要堆棧空間了。 編譯器可以選擇重用那些插槽,使得y和z共享相同的空間。 隨着優化的進行,它很簡單,但它並沒有獲得太多收益 - 它節省了一些空間,這在嵌入式系統中可能很重要,但在大多數.NET應用程序中並不重要。

作為旁注,VS2008版本中的C#編譯器甚至沒有執行最簡單的強度降低。 第一個版本的IL是這樣的:

L_0000: ldarg.0 
L_0001: ldc.i4.2 
L_0002: mul 
L_0003: stloc.0 
L_0004: ldarg.1 
L_0005: ldc.i4.3 
L_0006: mul 
L_0007: stloc.1 
L_0008: ldarg.2 
L_0009: ldc.i4.4 
L_000a: mul 
L_000b: stloc.2 
L_000c: ldloc.0 
L_000d: ldloc.1 
L_000e: add 
L_000f: ldloc.2 
L_0010: add 
L_0011: ret 

然而,我完全希望看到這個:

L_0000: ldarg.0 
L_0001: ldc.i4.2 
L_0002: mul 
L_0003: ldarg.1 
L_0004: ldc.i4.3 
L_0005: mul 
L_0006: add 
L_0007: ldarg.2 
L_0008: ldc.i4.4 
L_0009: mul 
L_000a: add 
L_000b: ret 

編譯器將執行您為您顯示的優化。

這是一種簡單的循環提升形式。

很多人為您提供IL,從性能角度向您展示您的兩個代碼片段實際上是相同的。 沒有必要去達到這種程度的細節,看看為什么會出現這種情況。 調用堆棧的角度考慮這個問題。

實際上,在包含代碼片段的方法的開頭會發生的事情是,您提供的代碼片段是編譯器將發出代碼以在方法的開頭為將在該方法中使用的所有本地分配空間。

在這兩種情況下,編譯器看到的是一個名為iTemp的本地,因此當它在堆棧上為本地分配空間時,它將分配32位來保存iTemp 編譯器在兩個代碼片段iTemp具有不同的范圍並不重要; 編譯器將通過不允許您在第一個片段中的for循環之外引用iTemp來強制執行該操作。 它將做的是分配這個空間一次(在方法的開頭)並在第一個片段的循環期間根據需要重用空間。

C#編譯器並不總是需要做好工作。 JIT優化器針對C#編譯器發出的IL進行了調整,更好看IL不會(必然)產生更好看的機器代碼。

我們來看一個早先的例子:

static int Func(int a, int b, int c)
{
    int x = a * 2;
    int y = b * 3;
    int z = c * 4;
    return x + y + z;
}

啟用了優化的3.5編譯器發出的IL如下所示:

.method private hidebysig static int32  Func(int32 a,
                                             int32 b,
                                             int32 c) cil managed
{
  // Code size       18 (0x12)
  .maxstack  2
  .locals init (int32 V_0,
           int32 V_1,
           int32 V_2)
  IL_0000:  ldarg.0
  IL_0001:  ldc.i4.2
  IL_0002:  mul
  IL_0003:  stloc.0
  IL_0004:  ldarg.1
  IL_0005:  ldc.i4.3
  IL_0006:  mul
  IL_0007:  stloc.1
  IL_0008:  ldarg.2
  IL_0009:  ldc.i4.4
  IL_000a:  mul
  IL_000b:  stloc.2
  IL_000c:  ldloc.0
  IL_000d:  ldloc.1
  IL_000e:  add
  IL_000f:  ldloc.2
  IL_0010:  add
  IL_0011:  ret
} // end of method test::Func

不是很理想吧? 我正在將它編譯成一個可執行文件,從一個簡單的Main方法調用它,編譯器沒有內聯它或真正進行任何優化。

那么運行時會發生什么?

事實上,JIT實際上是在調用Func()並生成比你在上面看到基於堆棧的IL時想象的更好的代碼:

mov     edx,dword ptr [rbx+10h]
mov     eax,1
cmp     rax,rdi
jae     000007ff`00190265

mov     eax,dword ptr [rbx+rax*4+10h]
mov     ecx,2
cmp     rcx,rdi
jae     000007ff`00190265

mov     ecx,dword ptr [rbx+rcx*4+10h]
add     edx,edx
lea     eax,[rax+rax*2]
shl     ecx,2
add     eax,edx
lea     esi,[rax+rcx]

暫無
暫無

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

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