繁体   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