如果声明vs if-else语句,哪个更快?

Jul*_*n__ 80 c++ performance assembly microbenchmark c++11

我前几天和朋友争论过这两个片段.哪个更快,为什么?

value = 5;
if (condition) {
    value = 6;
}
Run Code Online (Sandbox Code Playgroud)

和:

if (condition) {
    value = 6;
} else {
    value = 5;
}
Run Code Online (Sandbox Code Playgroud)

如果value是矩阵怎么办?

注意:我知道value = condition ? 6 : 5;存在并且我希望它更快,但它不是一个选项.

编辑(工作人员要求,因为问题暂时搁置):

  • 请通过考虑主流编译器(例如g ++,clang ++,vc,mingw)在优化和非优化版本或MIPS汇编中生成的x86汇编来回答.
  • 当汇编不同时,解释为什么版本更快和何时(例如"更好,因为没有分支和分支跟随问题blahblah")

Com*_*hip 274

TL; DR:if未经优化的代码中,else似乎没有更高效,但即使最基本的优化级别启用,代码也基本上被重写为value = condition + 5.


试了一下,为以下代码生成了程序集:

int ifonly(bool condition, int value)
{
    value = 5;
    if (condition) {
        value = 6;
    }
    return value;
}

int ifelse(bool condition, int value)
{
    if (condition) {
        value = 6;
    } else {
        value = 5;
    }
    return value;
}
Run Code Online (Sandbox Code Playgroud)

在gcc 6.3上,禁用了优化(-O0),相关的区别是:

 mov     DWORD PTR [rbp-8], 5
 cmp     BYTE PTR [rbp-4], 0
 je      .L2
 mov     DWORD PTR [rbp-8], 6
.L2:
 mov     eax, DWORD PTR [rbp-8]
Run Code Online (Sandbox Code Playgroud)

因为ifonly,虽然ifelse

 cmp     BYTE PTR [rbp-4], 0
 je      .L5
 mov     DWORD PTR [rbp-8], 6
 jmp     .L6
.L5:
 mov     DWORD PTR [rbp-8], 5
.L6:
 mov     eax, DWORD PTR [rbp-8]
Run Code Online (Sandbox Code Playgroud)

后者看起来效率稍差,因为它有一个额外的跳跃,但两者至少有两个,最多三个任务,所以除非你真的需要挤压每一滴性能(暗示:除非你正在航天飞机上工作,否则你没有,即便如此,你可能不这样做.差异不会明显.

但是,即使使用最低优化级别(-O1),两个函数都会减少到相同:

test    dil, dil
setne   al
movzx   eax, al
add     eax, 5
Run Code Online (Sandbox Code Playgroud)

这基本上相当于

return 5 + condition;
Run Code Online (Sandbox Code Playgroud)

假设condition为零或一.较高的优化级别并不会真正改变输出,除非它们设法避免在开始时movzx有效地将EAX寄存器置零.


免责声明:您可能不应该5 + condition自己编写(即使标准保证转换true为整数类型1),因为您的意图可能不会立即显示给阅读代码的人(可能包括您未来的自我).这段代码的要点是表明编译器在两种情况下产生的内容(实际上)是相同的.Ciprian Tomoiaga在评论中说得很好:

一个的工作就是写代码的人,让编译器用于编写代码的机器.

  • 这是一个很好的答案,应该被接受. (49认同)
  • @CiprianTomoiaga,除非你在写一个优化器,否则你不应该!在几乎所有情况下,您都应该让编译器执行这样的优化,特别是在它们严重降低代码可读性的情况下.只有当性能测试显示某些代码的问题时,您甚至应该开始尝试优化它,甚至然后保持它的清洁和良好评论,并且只执行产生可测量差异的优化. (26认同)
  • 我想回复Muzer,但它不会在线程中添加任何内容.但是,我只想重申一个**人**的工作就是为人类编写代码**并让**编译器**为机器**编写代码.我是从编译器开发人员PoV说的(我不是,但我对它们有所了解) (22认同)
  • 我永远不会使用添加(< - python对你做什么.) (10认同)
  • [值'true`转换为`int`总是产生1,句点.](https://timsong-cpp.github.io/cppwp/conv.integral#4)当然,如果你的条件只是"真实"而不是`bool`值`true`,那是完全不同的事情. (10认同)
  • @MatthieuM.定义"矩阵".这部分问题太难以回答了.在任何情况下,这个答案都充分描述了确定*any*值类型结果所需的技术,所有可能值类型的详尽列表(以及所有平台上的所有编译器,如果你想要彻底)既不可行也不有用.只需执行此处所做的相同分析,但使用"矩阵"或"小部件"或"双重"或其他任何内容(以及目标平台上的编译器). (3认同)

bol*_*lov 44

CompuChip的答案表明,因为int它们都针对同一个组件进行了优化,所以无关紧要.

如果值是矩阵怎么办?

我将以更一般的方式解释这一点,即如果value是一种类型,其构造和分配是昂贵的(并且移动是便宜的).

然后

T value = init1;
if (condition)
   value = init2;
Run Code Online (Sandbox Code Playgroud)

是次优的,因为在情况condition属实的情况下,您执行不必要的初始化init1,然后执行复制分配.

T value;
if (condition)
   value = init2;
else
   value = init3;
Run Code Online (Sandbox Code Playgroud)

这个更好.但如果默认构造很昂贵,并且复制构造比初始化更昂贵,那么仍然是次优的.

你有条件运算符解决方案是好的:

T value = condition ? init1 : init2;
Run Code Online (Sandbox Code Playgroud)

或者,如果您不喜欢条件运算符,则可以创建一个这样的辅助函数:

T create(bool condition)
{
  if (condition)
     return {init1};
  else
     return {init2};
}

T value = create(condition);
Run Code Online (Sandbox Code Playgroud)

取决于你是什么init1,init2你也可以考虑这个:

auto final_init = condition ? init1 : init2;
T value = final_init;
Run Code Online (Sandbox Code Playgroud)

但是,我必须再次强调,只有当给定类型的构造和分配真的很昂贵时,这才是相关的.即便如此,只有通过剖析你才能确定.

  • @plugwash考虑一个具有非常大的已分配数组的类.默认构造函数分配并初始化数组,这是昂贵的.移动(不是复制!)构造函数只能与源对象交换指针,而不需要分配或初始化大型数组. (6认同)
  • 昂贵的**和**没有优化出来.例如,如果默认构造函数将矩阵清零,则编译器可以意识到赋值只是覆盖那些0,因此根本不会为零,并直接写入此内存.当然,优化者是挑剔的野兽,所以很难预测他们什么时候开始...... (3认同)

zwo*_*wol 11

在伪汇编语言中,

    li    #0, r0
    test  r1
    beq   L1
    li    #1, r0
L1:
Run Code Online (Sandbox Code Playgroud)

可能也可能不快

    test  r1
    beq   L1
    li    #1, r0
    bra   L2
L1:
    li    #0, r0
L2:
Run Code Online (Sandbox Code Playgroud)

取决于实际CPU的复杂程度.从最简单到最高兴:

  • 对于大约1990年后制造的任何 CPU,良好的性能取决于指令缓存中的代码拟合.因此,如果有疑问,请尽量减少代码大小.这有利于第一个例子.

  • 有了一个基本的" 有序,五级流水线 "CPU,这仍然是你在许多微控制器中得到的CPU,每次采用分支条件或无条件时都会出现管道泡沫,因此最小化也很重要分支指令的数量.这也有利于第一个例子.

  • 一些更复杂的CPU - 足够用来执行" 无序执行 ",但不足以使用该概念的最着名的实现 - 可能在遇到写入后写入危险时引起管道泡沫.这有利于第二个例子,r0无论如何只写一次.这些CPU通常是足够看上处理在取指令无条件分支,所以你只是交易的写后读处罚分支处罚.

    我不知道是否有人还在制造这种CPU.然而,CPU的使用乱序执行的"最有名的实现"有可能削减对不太常用的指令角落,所以你必须要知道,这样的事情可能发生.一个真实的例子是Sandy Bridge CPU popcntlzcnt上的目标寄存器的错误数据依赖性.

  • 在最高端,OOO引擎将最终为两个代码片段发出完全相同的内部操作序列 - 这是"不用担心,编译器将以任何方式生成相同的机器代码"的硬件版本.但是,代码大小仍然很重要,现在您还应该担心条件分支的可预测性. 分支预测失败可能导致完整的管道冲洗,这对性能来说是灾难性的; 请参阅为什么处理排序数组比未排序数组更快?了解这可以带来多大的不同.

    如果分支非常不可预测的,你的CPU具有条件设定或有条件移动指令,这是使用它们的时间:

        li    #0, r0
        test  r1
        setne r0
    
    Run Code Online (Sandbox Code Playgroud)

    要么

        li    #0, r0
        li    #1, r2
        test  r1
        movne r2, r0
    
    Run Code Online (Sandbox Code Playgroud)

    条件集版本也比任何其他替代版本更紧凑; 如果该指令可用,则实际上保证对于这种情况是正确的事情,即使该分支是可预测的.条件移动版本需要一个额外的临时寄存器,并且总是浪费一条li指令的调度和执行资源; 如果分支实际上是可预测的,那么分支版本可能会更快.


Nei*_*eil 9

在未经优化的代码中,第一个示例总是分配一次变量,有时两次.第二个例子只分配一次变量.两个代码路径上的条件相同,因此无关紧要.在优化代码中,它取决于编译器.

与往常一样,如果您有关,请生成程序集并查看编译器实际执行的操作.


old*_*mer 8

什么会让你认为他们中的任何一个甚至一个班轮更快或更慢?

unsigned int fun0 ( unsigned int condition, unsigned int value )
{
    value = 5;
    if (condition) {
        value = 6;
    }
    return(value);
}
unsigned int fun1 ( unsigned int condition, unsigned int value )
{

    if (condition) {
        value = 6;
    } else {
        value = 5;
    }
    return(value);
}
unsigned int fun2 ( unsigned int condition, unsigned int value )
{
    value = condition ? 6 : 5;
    return(value);
}
Run Code Online (Sandbox Code Playgroud)

高级语言的更多代码行使编译器能够使用更多代码,因此如果您想对其进行一般规则,请为编译器提供更多代码.如果算法与上面的情况相同,那么人们会期望编译器具有最小的优化来计算出来.

00000000 <fun0>:
   0:   e3500000    cmp r0, #0
   4:   03a00005    moveq   r0, #5
   8:   13a00006    movne   r0, #6
   c:   e12fff1e    bx  lr

00000010 <fun1>:
  10:   e3500000    cmp r0, #0
  14:   13a00006    movne   r0, #6
  18:   03a00005    moveq   r0, #5
  1c:   e12fff1e    bx  lr

00000020 <fun2>:
  20:   e3500000    cmp r0, #0
  24:   13a00006    movne   r0, #6
  28:   03a00005    moveq   r0, #5
  2c:   e12fff1e    bx  lr
Run Code Online (Sandbox Code Playgroud)

并不是一个惊喜,它以不同的顺序执行第一个功能,但执行时间相同.

0000000000000000 <fun0>:
   0:   7100001f    cmp w0, #0x0
   4:   1a9f07e0    cset    w0, ne
   8:   11001400    add w0, w0, #0x5
   c:   d65f03c0    ret

0000000000000010 <fun1>:
  10:   7100001f    cmp w0, #0x0
  14:   1a9f07e0    cset    w0, ne
  18:   11001400    add w0, w0, #0x5
  1c:   d65f03c0    ret

0000000000000020 <fun2>:
  20:   7100001f    cmp w0, #0x0
  24:   1a9f07e0    cset    w0, ne
  28:   11001400    add w0, w0, #0x5
  2c:   d65f03c0    ret
Run Code Online (Sandbox Code Playgroud)

希望你能得到这个想法,如果不明显的是不同的实现实际上没有不同,你可以尝试这个.

就矩阵而言,不确定这是多么重要,

if(condition)
{
 big blob of code a
}
else
{
 big blob of code b
}
Run Code Online (Sandbox Code Playgroud)

只是将相同的if-then-else包装器放在大块代码周围,它们值= 5或更复杂的东西.同样地,比较即使它是一大块代码,它仍然必须被计算,并且等于或不等于某些东西通常用负数编译,如果(条件)做​​某事通常被编译为好像不是条件goto.

00000000 <fun0>:
   0:   0f 93           tst r15     
   2:   03 24           jz  $+8         ;abs 0xa
   4:   3f 40 06 00     mov #6, r15 ;#0x0006
   8:   30 41           ret         
   a:   3f 40 05 00     mov #5, r15 ;#0x0005
   e:   30 41           ret         

00000010 <fun1>:
  10:   0f 93           tst r15     
  12:   03 20           jnz $+8         ;abs 0x1a
  14:   3f 40 05 00     mov #5, r15 ;#0x0005
  18:   30 41           ret         
  1a:   3f 40 06 00     mov #6, r15 ;#0x0006
  1e:   30 41           ret         

00000020 <fun2>:
  20:   0f 93           tst r15     
  22:   03 20           jnz $+8         ;abs 0x2a
  24:   3f 40 05 00     mov #5, r15 ;#0x0005
  28:   30 41           ret         
  2a:   3f 40 06 00     mov #6, r15 ;#0x0006
  2e:   30 41
Run Code Online (Sandbox Code Playgroud)

我们刚刚和其他人一起在stackoverflow上进行了这个练习.这个mips编译器很有趣,在这种情况下不仅实现了功能相同,而且只有一个函数只是跳转到另一个以节省代码空间.虽然不是这样做的

00000000 <fun0>:
   0:   0004102b    sltu    $2,$0,$4
   4:   03e00008    jr  $31
   8:   24420005    addiu   $2,$2,5

0000000c <fun1>:
   c:   0004102b    sltu    $2,$0,$4
  10:   03e00008    jr  $31
  14:   24420005    addiu   $2,$2,5

00000018 <fun2>:
  18:   0004102b    sltu    $2,$0,$4
  1c:   03e00008    jr  $31
  20:   24420005    addiu   $2,$2,5
Run Code Online (Sandbox Code Playgroud)

更多目标.

00000000 <_fun0>:
   0:   1166            mov r5, -(sp)
   2:   1185            mov sp, r5
   4:   0bf5 0004       tst 4(r5)
   8:   0304            beq 12 <_fun0+0x12>
   a:   15c0 0006       mov $6, r0
   e:   1585            mov (sp)+, r5
  10:   0087            rts pc
  12:   15c0 0005       mov $5, r0
  16:   1585            mov (sp)+, r5
  18:   0087            rts pc

0000001a <_fun1>:
  1a:   1166            mov r5, -(sp)
  1c:   1185            mov sp, r5
  1e:   0bf5 0004       tst 4(r5)
  22:   0204            bne 2c <_fun1+0x12>
  24:   15c0 0005       mov $5, r0
  28:   1585            mov (sp)+, r5
  2a:   0087            rts pc
  2c:   15c0 0006       mov $6, r0
  30:   1585            mov (sp)+, r5
  32:   0087            rts pc

00000034 <_fun2>:
  34:   1166            mov r5, -(sp)
  36:   1185            mov sp, r5
  38:   0bf5 0004       tst 4(r5)
  3c:   0204            bne 46 <_fun2+0x12>
  3e:   15c0 0005       mov $5, r0
  42:   1585            mov (sp)+, r5
  44:   0087            rts pc
  46:   15c0 0006       mov $6, r0
  4a:   1585            mov (sp)+, r5
  4c:   0087            rts pc

00000000 <fun0>:
   0:   00a03533            snez    x10,x10
   4:   0515                    addi    x10,x10,5
   6:   8082                    ret

00000008 <fun1>:
   8:   00a03533            snez    x10,x10
   c:   0515                    addi    x10,x10,5
   e:   8082                    ret

00000010 <fun2>:
  10:   00a03533            snez    x10,x10
  14:   0515                    addi    x10,x10,5
  16:   8082                    ret
Run Code Online (Sandbox Code Playgroud)

和编译器

使用此i代码,可以预期不同的目标也匹配

define i32 @fun0(i32 %condition, i32 %value) #0 {
  %1 = icmp ne i32 %condition, 0
  %. = select i1 %1, i32 6, i32 5
  ret i32 %.
}

; Function Attrs: norecurse nounwind readnone
define i32 @fun1(i32 %condition, i32 %value) #0 {
  %1 = icmp eq i32 %condition, 0
  %. = select i1 %1, i32 5, i32 6
  ret i32 %.
}

; Function Attrs: norecurse nounwind readnone
define i32 @fun2(i32 %condition, i32 %value) #0 {
  %1 = icmp ne i32 %condition, 0
  %2 = select i1 %1, i32 6, i32 5
  ret i32 %2
}


00000000 <fun0>:
   0:   e3a01005    mov r1, #5
   4:   e3500000    cmp r0, #0
   8:   13a01006    movne   r1, #6
   c:   e1a00001    mov r0, r1
  10:   e12fff1e    bx  lr

00000014 <fun1>:
  14:   e3a01006    mov r1, #6
  18:   e3500000    cmp r0, #0
  1c:   03a01005    moveq   r1, #5
  20:   e1a00001    mov r0, r1
  24:   e12fff1e    bx  lr

00000028 <fun2>:
  28:   e3a01005    mov r1, #5
  2c:   e3500000    cmp r0, #0
  30:   13a01006    movne   r1, #6
  34:   e1a00001    mov r0, r1
  38:   e12fff1e    bx  lr


fun0:
    push.w  r4
    mov.w   r1, r4
    mov.w   r15, r12
    mov.w   #6, r15
    cmp.w   #0, r12
    jne .LBB0_2
    mov.w   #5, r15
.LBB0_2:
    pop.w   r4
    ret

fun1:
    push.w  r4
    mov.w   r1, r4
    mov.w   r15, r12
    mov.w   #5, r15
    cmp.w   #0, r12
    jeq .LBB1_2
    mov.w   #6, r15
.LBB1_2:
    pop.w   r4
    ret


fun2:
    push.w  r4
    mov.w   r1, r4
    mov.w   r15, r12
    mov.w   #6, r15
    cmp.w   #0, r12
    jne .LBB2_2
    mov.w   #5, r15
.LBB2_2:
    pop.w   r4
    ret
Run Code Online (Sandbox Code Playgroud)

从技术上讲,这些解决方案中有一些性能存在差异,有时结果是5个案例跳过结果是6个代码,反之亦然,是一个比执行更快的分支?人们可以争辩,但执行应该有所不同.但是这更像是一个if条件vs如果没有条件在代码中导致编译器执行如果这跳过else执行通过.但这不一定是由于编码风格,而是由于比较以及if和else的情况在任何语法中.