【问题标题】:How does the C# compiler optimize a code fragment?C# 编译器如何优化代码片段?
【发布时间】:2010-01-01 20:03:28
【问题描述】:

如果我有这样的代码

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

编译器是否将 iTemp 实例化 10 次?

还是优化一下?

我的意思是如果我将循环重写为

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

会更快吗?

【问题讨论】:

  • ints 不是对象,因此创建它们可能是一两条指令。也许一个对象会是一个更好的例子。
  • 我喜欢第二种方法。为什么你首先需要这个额外的变量?另外,请在 for 循环中添加一些空格!
  • 最后,比较生成的 IL 和 PROFILE 总是一个好主意!
  • @mmyers: ints 绝对是 C# 中的对象。
  • 注意不要混淆您所说的实施级别。从 C# 语言的角度来看,int 是对象。从虚拟执行系统的角度来看,int 是包含 32 位的桶,对象是垃圾收集堆中的托管地址。

标签: c# optimization compiler-construction


【解决方案1】:

使用reflector可以查看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 时不会产生性能差异。

【讨论】:

  • 在他展示的特定短代码摘录中,这是正确的,但在更大的方法中,它可能会有所不同。主要区别在于,在第二个代码示例中,变量 iTemp 在循环之后将可用,而在第一个代码示例中,它不会。所以“或其他”并不完全正确。
  • @Lasse V. Karlsen:正确且已修复。
【解决方案2】:

正如其他人所说,您显示的代码会产生等效的 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() ) );

【讨论】:

  • 我有一个小的转录错误 - 将初始值保留为 -1 并在粘贴代码时以某种方式搞砸了 string.Join()。如果您之前有编译错误,请尝试新版本。
【解决方案3】:

如果您真的对 CSC(C# 编译器)如何处理您的代码感到好奇,您可能想尝试一下LINQPad- 它允许您输入简短的 C# 表达式或程序并查看在生成的 IL(CLR 字节码)处。

【讨论】:

    【解决方案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;
     }
    

    忽略这可以很容易地优化为 return (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 
    

    【讨论】:

    • 抖动的工作就是做这些优化。
    • 真的吗?我认为当你贪婪地掌握一棵解析树时,这是最好的处理方式。
    【解决方案5】:

    编译器将为您执行您显示的优化。

    这是一种简单的循环提升形式。

    【讨论】:

      【解决方案6】:

      很多人向您提供 IL 以向您展示您的两个代码片段从性能角度来看实际上是相同的。没有必要深入到那个详细程度来了解为什么会这样。从call stack的角度想想吧。

      实际上,在包含您提供的两个代码片段的方法开头发生的情况是,编译器将发出代码以在方法开头为将在该方法中使用的所有局部变量分配空间。

      在这两种情况下,编译器看到的是一个名为 iTemp 的局部变量,因此当它在堆栈上为局部变量分配空间时,它将分配 32 位来保存 iTemp。对于编译器来说,两个代码片段iTemp 具有不同的范围并不重要;编译器将通过不允许您在第一个片段中的 for 循环之外引用 iTemp 来强制执行此操作。它将做的是分配这个空间一次(在方法的开头),并在第一个片段的循环期间根据需要重用空间。

      【讨论】:

        【解决方案7】:

        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]
        

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2016-02-14
          • 2013-09-11
          • 2013-03-11
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多