在托管语言中避免分支机构

时间:2011-11-08 07:49:21

标签: c# managed-code

C 中,当编译到x86机器时,我通常会用逻辑表达式替换分支,当速度是最重要的方面时,即使条件很复杂,例如,而不是< / p>

char isSomething() {
    if (complexExpression01) {
        if (complexExpression02) {
            if(!complexExpression03) {
                return 1;
            }
        }
    }
    return 0;
}

我会写:

char isSomething() {
    return complexExpression01 &&
           complexExpression02 &&
           !complexExpression03 ;
}

现在显然,这可能更难维护,代码可读性更低,但实际上可能更快。

在使用托管代码(例如C#)时,是否有任何理由采取相同的方式?托管代码中的“跳转”是否昂贵,因为它们处于非托管代码中(至少在x86上)?

4 个答案:

答案 0 :(得分:4)

一般

在常规编译器中,生成的代码通常是相同的,至少在假设您使用常规

csc.exe /optimize+
cl.exe /O2
g++ -O2

和相关的默认优化模式。

一般的口头禅是:个人资料,个人资料,个人资料(并且在您的探查者告诉您之前不要进行微观优化)。您始终可以查看生成的代码 2 ,看看是否还有改进的余地。

以这种方式思考,例如C#代码:

C#/。NET

每个 complexExpressions 都是一个事实上的函数调用调用(call,calli,callvirt opcode 3 ),它要求将其参数压入堆栈。返回值将被推入堆栈而不是退出时的参数。

现在,CLR是一个基于堆栈的虚拟机(即无寄存器),这与堆栈上的匿名临时变量完全相同。唯一的区别是代码中使用的标识符数量。

现在JIT引擎对此做了什么是另一回事:JIT引擎必须将这些调用转换为本机程序集,并且可能通过调整寄存器分配,指令排序,分支预测和类似之类的东西来进行优化。 1

<子> 1 (虽然在实践中,对于此示例,不允许进行更有趣的优化,因为complex function calls可能有副作用,C#规范非常清楚评估顺序和所谓的序列点。 注意但是,JIT引擎允许内联函数调用,以减少调用开销。

不仅当它们是非虚拟的,而且(IIRC)也可以在编译时静态地知道某些.NET框架内部的运行时类型。我必须为此查找一个参考,但事实上我认为.NET framework 4.0中引入了一些属性来明确阻止框架函数的内联;这样,Microsoft就可以在服务包/更新中修补库代码,即使用户程序集已经提前编译(ngen.exe)到本机映像中也是如此。

C / C ++

在C / C ++中,内存模型更加宽松(即至少在C ++ 11之前),并且代码通常在编译时直接编译为本机指令。补充说C / C ++编译器通常会进行积极的内联,即使在这样的编译器中代码通常都是相同的,除非你在没有启用优化的情况下进行编译


<子> 2 我用

  • ildasmmonodis查看生成的IL代码
  • mono -aot=full,staticmkbundle生成本机对象模块,objdump -CdS查看带注释的本机程序集说明。

请注意,这纯粹是好奇心,因为我很少发现这种有趣的瓶颈。但是,请参阅J on Skeet's blog posts on performance optimizing Noda.NET,了解可能潜伏在生成的通用类IL代码中的惊喜示例。

3 编辑对编译器内在函数的运算符不准确,但即使它们只是将结果留在堆栈上。

答案 1 :(得分:2)

这取决于托管语言的CLR和编译器的实现。对于C#,以下测试用例证明嵌套if语句和组合if语句的指令没有区别:

            // case 1
            if (value1 < value2)
00000089  mov         eax,dword ptr [ebp-0Ch] 
0000008c  cmp         eax,dword ptr [ebp-10h] 
0000008f  jge         000000A6 
            {
                if (value2 < value3)
00000091  mov         eax,dword ptr [ebp-10h] 
00000094  cmp         eax,dword ptr [ebp-14h] 
00000097  jge         000000A6 
                {
                    result1 = true;
00000099  mov         eax,1 
0000009e  and         eax,0FFh 
000000a3  mov         dword ptr [ebp-4],eax 
                }
            }

            // case 2
            if (value1 < value2 && value2 < value3)
000000a6  mov         eax,dword ptr [ebp-0Ch] 
000000a9  cmp         eax,dword ptr [ebp-10h] 
000000ac  jge         000000C3 
000000ae  mov         eax,dword ptr [ebp-10h] 
000000b1  cmp         eax,dword ptr [ebp-14h] 
000000b4  jge         000000C3 
            {
                result2 = true;
000000b6  mov         eax,1 
000000bb  and         eax,0FFh 
000000c0  mov         dword ptr [ebp-8],eax 
            }

答案 2 :(得分:1)

这两个表达式将导致相同数量的测试,因为逻辑和运算符(&&)在C和C#中都有短路语义。你的问题的前提(表示程序的第二种方式导致较少的分支)因此是不正确的。

答案 3 :(得分:0)

要知道的唯一方法是衡量。

CLR将真和假表示为1和0,因此如果使用逻辑表达式有一些优势,我不会感到惊讶。我们来看看:

static void BenchBranch() {
    Stopwatch sw = new Stopwatch();

    const int NMAX = 1000000000;
    bool a = true;
    bool b = false;
    bool c = true;

    sw.Restart();
    int sum = 0;
    for (int i = 0; i < NMAX; i++) {
        if (a)
            if (b)
                if (c)
                    sum++;
        a = !a;
        b = a ^ b;
        c = b;
    }
    sw.Stop();
    Console.WriteLine("1: {0:F3} ms ({1})", sw.Elapsed.TotalMilliseconds, sum);

    sw.Restart();
    sum = 0;
    for (int i = 0; i < NMAX; i++) {
        if (a && b && c) 
            sum++;
        a = !a;
        b = a ^ b;
        c = b;
    }
    sw.Stop();
    Console.WriteLine("2: {0:F3} ms ({1})", sw.Elapsed.TotalMilliseconds, sum);

    sw.Restart();
    sum = 0;
    for (int i = 0; i < NMAX; i++) {
        sum += (a && b && c) ? 1 : 0;
        a = !a;
        b = a ^ b;
        c = b;
    }
    sw.Stop();
    Console.WriteLine("3: {0:F3} ms ({1})", sw.Elapsed.TotalMilliseconds, sum);
}

结果:

1:  2713.396 ms (250000000)
2:  2477.912 ms (250000000)
3:  2324.916 ms (250000000)

因此,使用逻辑运算符而不是嵌套条件语句似乎有一点点优势。但是,任何特定的实例都可能会产生不同的结果。

最后,这样的微优化是否值得,取决于代码的性能关键程度。