C#中编译器优化质量差

3

我不知道为什么,但我观察了由标准C#编译器(VS2015)生成的IL代码,在发布模式下非常不优化。

我测试的代码非常简单:

static void Main(string[] args)
    {
        int count = 25 + 7/3;
        count += 100;
        Console.WriteLine("{0}", count);
    }

在调试模式下,IL输出为:
// [12 9 - 12 10]
IL_0000: nop          

// [34 13 - 34 34]
IL_0001: ldc.i4.s     27 // 0x1b
IL_0003: stloc.0      // count

// [35 13 - 35 26]
IL_0004: ldloc.0      // count
IL_0005: ldc.i4.s     100 // 0x64
IL_0007: add          
IL_0008: stloc.0      // count

// [36 13 - 36 45]
IL_0009: ldstr        "{0}"
IL_000e: ldloc.0      // count
IL_000f: box          [mscorlib]System.Int32
IL_0014: call         void [mscorlib]System.Console::WriteLine(string, object)
IL_0019: nop          

// [37 9 - 37 10]
IL_001a: ret          

发布模式下的代码为:

 IL_0000: ldc.i4.s     27 // 0x1b
IL_0002: stloc.0      // V_0
IL_0003: ldloc.0      // V_0
IL_0004: ldc.i4.s     100 // 0x64
IL_0006: add          
IL_0007: stloc.0      // V_0
IL_0008: ldstr        "{0}"
IL_000d: ldloc.0      // V_0
IL_000e: box          [mscorlib]System.Int32
IL_0013: call         void [mscorlib]System.Console::WriteLine(string, object)
IL_0018: ret  

现在,为什么编译器不执行求和(27 + 100)并直接调用WriteLine输出127?

我在C++中尝试了同样的示例,结果符合预期。

是否有一些特殊标志可以执行这种优化?

更新: 我在MONO 4.6.20上尝试了相同的代码,发布模式下的结果如下:

 // method line 2
.method private static hidebysig
       default void Main (string[] args)  cil managed
{
    // Method begins at RVA 0x2058
    .entrypoint
    // Code size 18 (0x12)
    .maxstack 8
    IL_0000:  ldstr "{0}"
    IL_0005:  ldc.i4.s 0x7f
    IL_0007:  box [mscorlib]System.Int32
    IL_000c:  call void class [mscorlib]System.Console::WriteLine(string, ob                                                                                                                               ject)
    IL_0011:  ret
} // end of method Program::Main

是的,我也已经禁用了额外的调试或跟踪输出。 - Linefinc
14
你看过 JIT 编译的代码吗?在 .NET 中,大部分真正的优化都是由 JIT 进行的,而不是 C# 编译器。 - Jon Skeet
不,我不知道如何获取最终的x86代码。 - Linefinc
1
通常情况下,这很棘手(我已经有一段时间没有使用cordbg或windbg了)-但是你可以使用手动优化对代码进行基准测试。如果它的表现方式相同,那么你可能不在意-或者更确切地说,我个人更喜欢当前生成的IL,因为它使得即使在调试器中释放代码也更容易跟踪。 - Jon Skeet
2
注意@Kyle的指示,您必须确保要查看反汇编的代码已经执行了至少一次,这样代码就已经被JIT编译器编译过了,然后再附加调试器。如果在调试器已经附加的情况下进行JIT编译,将会生成不同的汇编代码。 - Scott Chamberlain
显示剩余5条评论
1个回答

4
编译器生成的中间语言(IL)输出不能可靠地评估代码的优化程度,因为Just-In-Time (JIT) 编译器会在运行时使用IL来生成实际的可执行代码。在这种情况下,在Release模式下,不指定偏好32位的Any CPU架构上, JIT产生的实际x64代码如下所示:
sub         rsp,28h  
mov         rcx,7FFF85323E98h  
call        00007FFF91C72530  ; I'm not sure what this call does, I assume it's allocating memory for the boxed int
mov         rcx,20CA5CB3648h  
mov         rcx,qword ptr [rcx]  ; After this rcx is actually pointing to the string "{0}"
mov         dword ptr [rax+8],7Fh ; Box the value 127 into the object that rax points at
mov         rdx,rax  
call        00007FFF85160070  ; Call Console.WriteLine with its arguments in rcx and rdx
nop  
add         rsp,28h  
ret  

所以额外版本被省略了。
如果我打开“优先使用32位”,生成的x86代码如下:
mov         ecx,72041638h  
call        011630F4  ; presumably allocating memory for the boxed int
mov         edx,eax  
mov         eax,dword ptr ds:[40E232Ch] ; loads a pointer to "{0}" into eax
mov         dword ptr [edx+4],7Fh  ; boxes 127 into object pointed at by edx
mov         ecx,eax  
call        71F373F4  ; calls Console.WriteLine with arguments in ecx and edx
ret  

在这两种情况下,JIT都优化了本地变量以及额外的加法操作。由于JIT执行了很多优化,你会发现C#编译器本身并没有太大的优化工作。
简而言之,从C#编译器生成的IL并非机器运行的代码,因此通常不代表将应用的优化类型。

JIT能否执行程序级别的优化,还是其范围仅限于类/方法? - rollsch

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接