6

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

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

我会写:

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

现在很明显,这可能是一个更难维护且可读性更低的代码,但它实际上可能更快。

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

4

4 回答 4

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 框架 4.0 中引入了一些属性来明确防止框架函数的内联;这样微软就可以在服务包/更新中修补库代码,即使用户程序集已提前编译 (ngen.exe) 为本机映像。

C/C++

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


2我用

  • ildasmmonodis查看生成的 IL 代码
  • mono -aot=full,staticmkbundle生成本机对象模块并objdump -CdS查看带注释的本机汇编指令。

请注意,这纯粹是出于好奇,因为我很少会以这种方式发现有趣的瓶颈。但是,请参阅 J on Skeet 的关于性能优化的博客文章,Noda.NET以了解可能潜伏在生成的通用类 IL 代码中的惊喜的好例子。

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

于 2011-11-08T07:51:52.707 回答
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 
            }
于 2011-11-08T08:12:03.160 回答
1

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

于 2011-11-08T07:59:57.733 回答
0

唯一知道的方法是测量。

True 和 false 由 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)

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

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

于 2011-11-08T08:13:39.450 回答