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

问题描述 投票:0回答:7

如果我有这样的代码

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# optimization
7个回答
20
投票

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


4
投票

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

3
投票

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


3
投票

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

考虑:

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



1
投票

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


0
投票
调用堆栈

的角度来思考这个问题即可。 实际上,在包含代码片段(如您提供的两个代码片段)的方法开头发生的情况是,编译器将发出代码,在方法开头为将在该方法中使用的所有局部变量分配空间。

在这两种情况下,编译器看到的都是名为

iTemp

的局部变量,因此当它在堆栈上为局部变量分配空间时,它将分配 32 位来保存

iTemp
。对于编译器来说,两个代码片段
iTemp
具有不同的范围并不重要;编译器将通过不允许您在第一个片段中的
iTemp
循环之外引用
for
来强制执行这一点。它将执行的操作是分配该空间一次(在方法的开头),并在第一个片段的循环期间根据需要重用该空间。
    


0
投票

让我们举一个之前的例子:

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]

	
© www.soinside.com 2019 - 2024. All rights reserved.