"if if"和"if else if"的性能差异

34

我在思考C/C++中以下两种语句是否有性能差异:

情况1:

if (p==0)
   do_this();
else if (p==1)
   do_that();
else if (p==2)
   do_these():

情况2:

if(p==0)
    do_this();
if(p==1)
    do_that();
if(p==2)
    do_these();

5
进行基准测试是现代理解事物运作的替代品吗? - Michael Krelin - hacker
9
不仅是性能,而且意义完全不同!想象一下,p 重载了 operator==,始终返回 true,而 do_that() 将所有的钱都发送给了你的前妻。 - Kerrek SB
11
只需关注编写清晰、简洁、健壮和可靠的代码,担心这种微小的微观优化是低效且毫无意义的。 - Paul R
7
虽然总的来说你是正确的,但这个问题仍然是一个有效且有趣的问题,了解计算机的真正工作原理从未是无用的(只要不让这些问题成为你编码的主要指南即可,而这并不是此处的提问者意图)。@PaulR - Christian Rau
10
如果人们把所有用于抨击“微优化”的时间都用来编写清晰、简明、强健和可靠的代码,那么它也会“微优化”。 - Michael Krelin - hacker
显示剩余15条评论
8个回答

39

假设简单类型(在本例中,我使用了int)并且没有什么奇怪的操作(没有重新定义int的operator=),至少在AMD64架构上运行的GCC 4.6中,这两者没有区别。生成的代码是相同的:

0000000000000000 <case_1>:                                   0000000000000040 <case_2>:
   0:   85 ff                   test   %edi,%edi               40:   85 ff                   test   %edi,%edi
   2:   74 14                   je     18 <case_1+0x18>        42:   74 14                   je     58 <case_2+0x18>
   4:   83 ff 01                cmp    $0x1,%edi               44:   83 ff 01                cmp    $0x1,%edi
   7:   74 27                   je     30 <case_1+0x30>        47:   74 27                   je     70 <case_2+0x30>
   9:   83 ff 02                cmp    $0x2,%edi               49:   83 ff 02                cmp    $0x2,%edi
   c:   74 12                   je     20 <case_1+0x20>        4c:   74 12                   je     60 <case_2+0x20>
   e:   66 90                   xchg   %ax,%ax                 4e:   66 90                   xchg   %ax,%ax
  10:   f3 c3                   repz retq                      50:   f3 c3                   repz retq 
  12:   66 0f 1f 44 00 00       nopw   0x0(%rax,%rax,1)        52:   66 0f 1f 44 00 00       nopw   0x0(%rax,%rax,1)
  18:   31 c0                   xor    %eax,%eax               58:   31 c0                   xor    %eax,%eax
  1a:   e9 00 00 00 00          jmpq   1f <case_1+0x1f>        5a:   e9 00 00 00 00          jmpq   5f <case_2+0x1f>
  1f:   90                      nop                            5f:   90                      nop
  20:   31 c0                   xor    %eax,%eax               60:   31 c0                   xor    %eax,%eax
  22:   e9 00 00 00 00          jmpq   27 <case_1+0x27>        62:   e9 00 00 00 00          jmpq   67 <case_2+0x27>
  27:   66 0f 1f 84 00 00 00    nopw   0x0(%rax,%rax,1)        67:   66 0f 1f 84 00 00 00    nopw   0x0(%rax,%rax,1)
  2e:   00 00                                                  6e:   00 00 
  30:   31 c0                   xor    %eax,%eax               70:   31 c0                   xor    %eax,%eax
  32:   e9 00 00 00 00          jmpq   37 <case_1+0x37>        72:   e9 00 00 00 00          jmpq   77 <case_2+0x37>
  37:   66 0f 1f 84 00 00 00    nopw   0x0(%rax,%rax,1)
  3e:   00 00 

case_1末尾的额外指令只是用于填充(以便对齐下一个函数)

这并不奇怪,因为确定在该函数中未更改p是一种相当基本的优化。如果p可以被更改(例如通过引用传递或作为指向各个do_...函数的指针,或者p本身是引用或指针,因此可能存在别名),则其行为是不同的,生成的代码也会不同。


1
无论如何,无法重新定义intoperator==(或operator=)。 - L. F.

22

在前一种情况下,匹配后的条件不会被评估。


3
该男子说了实话。在最坏的情况下,它们将是相同的。否则,如果条件有多个,使用if-else-if会更快,因为一旦满足一个条件,其余条件将不会被评估。 - Ayush
2
@xbonez:永远不要低估编译器...如果两个代码片段最终生成相同的汇编代码(在C语言中,==不能被重载),我不会感到惊讶,因为编译器可以推断只有一个条件是真的(假设if-branch不修改正在测试的值)。也就是说,假设两个版本的结果相同且类型是基本类型,编译器可以将它们都转换为switch。 - David Rodríguez - dribeas
5
不要高估编译器,它们甚至不知道代码是否具有相同的含义。 - Michael Krelin - hacker
2
好的观点 - 如果每个块都被允许修改 p ...(并尝试证明指向 p 的指针没有被存储在磁盘上,并且可以被这些函数检索)。 - Kerrek SB
@Kerrek: 逃逸分析通常可以证明未存储任何指针,尽管有很多情况实际上并非如此,但是该分析无法证明它。如果编译器可以内联 do_this(),则可以从另一个方面来处理这个问题,这就是为什么严格的别名规则很重要--如果 p 是一个 int,而 do_this() 仅通过指针修改 float 变量,则编译器可以假设 do_this() 不会修改 p - Steve Jessop
显示剩余3条评论

18

if else 更快; 如果在最后一个if之前找到了匹配项,则至少跳过最后一个if语句;如果在第一个if中找到匹配项,则会跳过所有其他语句。

if if 较慢; 即使使用第一个if语句找到了匹配项,它仍将继续尝试在其他语句中进行匹配。


5
什么?已经写于两年前的被接受回答表明编译器知道如果 if (p == 0) 成立,那么 p 不能有其他值,所以这些测试会被跳过。 - Bo Persson
3
@BoPersson我的回答是逻辑上的,不针对任何特定的编程语言或硬件,就像已被接受的答案一样。 - Waqleh
带有返回值的 if 代码块与 if-else 相同。 - its4zahoor
我怀疑语言优化器会读取那个代码块并创建一个switch语句。 - Derek Wade

8
是的,性能差异如下所示:
第二个语句会评估每一个IF。

5

正如已经展示的那样……它是多变的。

如果我们谈论原始(内置)类型,比如int,那么编译器可能足够智能,使得它不重要(或者不是)。无论如何,性能影响将很小,因为调用函数的成本比一个if语句高得多,所以如果你试图衡量它,差异很可能会在噪声中消失。

然而,语义上有很大的区别。

当我看到第一个例子时:

if (...) {
  // Branch 1
} else if (...) {
  // Branch 2
}

那么我知道无论这两个分支中的哪一个被执行,只会有一个被执行。

然而,当我阅读第二种情况:

if (...) {
}
if (...) {
}

然后我不得不想知道是否有可能采取两个分支,这意味着我必须仔细检查第一段代码,以确定它是否可能影响第二个测试。当我最终得出结论并且发现没有影响时,我会咒骂那些懒得写那该死的else语句的开发者,这将使我节省最后10分钟的时间。
因此,请注意正确和明确地表达语义,以帮助你自己和未来的维护人员。
在这个问题上,有人可能会认为,也许这个派遣逻辑可以用其他结构更好地表达,比如一个switch或者一个map?(要小心后者并避免过度设计 ;))

3
您可能不会注意到在这么少的表达式中性能上的任何差别。但从理论上讲,if..if..if 需要检查每一个表达式。如果单个表达式是互斥的,您可以使用 if..else if.. 来节省这种评估。这样只有在前面的情况失败时,才会检查其他表达式。
请注意,当仅检查整数是否相等时,也可以使用 switch 语句。这样,即使对长序列进行检查,您仍然可以保持一定程度的可读性。
switch ( p )
{
    case 0:
        do_this();
        break;

    case 1:
        do_that();
        break;

    case 2:
        do_these():
        break;
}

1

如果..如果情况可以通过在所有后续的IF检查中使用DONE标志来改进(因为真实逻辑是从左到右评估的),以避免重复工作/匹配和优化。

bool bDone = false;

If( <condition> ) {
    <work>;
    bDone = true;
}
if (!bDone && <condition> ) {
    <work>;
    bDone = true;
}

或者可以使用类似这样的逻辑:

While(true) {
    if( <condition> ) {
        <work>;
        break;
    }
    if( <condition> ) {
        <work>;
        break;
    }
    ....
    break;
}

虽然阅读起来有些混乱(问“为什么要这样做”)


0
主要区别在于if/else结构一旦其中一个返回true,就会停止评估ifs()。这意味着它可能只执行1或2个ifs,然后退出。另一个版本将检查所有3个ifs,无论其他ifs的结果如何。
所以... if/else的操作成本为“最多3次检查”。if/if/if版本的操作成本为“总是执行3次检查”。假设正在检查的所有3个值同等可能,if/else版本将平均执行1.5个ifs,而if/if版本将始终执行3个ifs。从长远来看,“else”结构可以节省1.5个ifs的CPU时间。

2
除非优化器足够聪明并且可以证明在do_this()或者do_that()期间,p值没有发生改变,否则代码总是执行3次检查。如果在if/if的情况下,并且假设没有重载操作符,那么优化器可以意识到,如果p==0为真,那么p==1不可能也为真,并跳过对它的评估,就像代码是if/else一样。抽象机执行了3次检查,但实现是否这样做是另外一回事。 - Steve Jessop

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接