C#编译器如何优化代码片段?

C#编译器如何优化代码片段?,c#,optimization,compiler-construction,C#,Optimization,Compiler Construction,如果我有这样的代码 for(int i=0;i<10;i++) { int iTemp; iTemp = i; //......... } for(inti=0;i编译器将执行您所展示的优化 这是循环提升的一种简单形式。如果你真的对CSC(C#编译器)如何处理你的代码感到好奇,你可能想玩一下——它允许你输入简短的C#表达式或程序,并查看生成的IL(CLR字节码)。使用它,你可以查看C#编译器生成的IL .method private hidebysig stati

如果我有这样的代码

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

for(inti=0;i编译器将执行您所展示的优化


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

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

使用它,你可以查看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表达式捕获变量以供以后执行。在这种情况下,代码不同,因为它必须跟踪e在其他情况下,可能也没有进行优化

当您想要捕获lambda表达式的值时,创建循环变量的新副本是一种常见的技术

尝试:

var a=新列表{0,1,2,3,4,5,6,7,8,9};
var q=a.AsEnumerable();
int iTemp;
对于(int i=0;i x x.ToString()).ToArray()),
q、 计数());

var a=新列表{0,1,2,3,4,5,6,7,8,9};
var q=a.AsEnumerable();
对于(int i=0;i x x.ToString()).ToArray()),
q、 计数());

需要记住的一点是,局部变量通常是在堆栈上分配的。编译器必须完成的一项任务是计算出特定方法需要多少堆栈空间,并将其放在一边

考虑:

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]

int
s不是对象,因此创建它们可能需要一两条指令。也许一个物体会是一个更好的例子。我喜欢第二种方法。为什么首先需要这个额外的变量?另外,请为循环添加一些空格!最后,比较生成的IL和配置文件总是一个好主意@mmyers:
int
s绝对是C#中的对象。小心不要混淆您所说的实现级别。从C语言的角度来看,int是对象。从虚拟执行系统的角度来看,int是包含32位的bucket,而对象是垃圾收集堆中的托管地址。我有一个小的转录错误——将初始值保留为-1,并在粘贴代码时以某种方式将字符串.Join()搞乱了。如果您以前有编译错误,请尝试新版本。jitter的工作就是做这些优化。真的吗?我认为,当你贪婪地掌握一棵解析树时,这是最好的处理方法。在他展示的特定简短代码摘录中,这是正确的,但在更大的方法中,它可能会有所不同。主要区别在于,在第二个代码示例中,变量iTemp将在循环之后可用,而在第一个代码示例中则不可用。所以“否则”并不完全正确。@Lasse V.Karlsen:正确且已修复。
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;
}
.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
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]