if 语句 vs if-else 语句,哪个更快?

If statement vs if-else statement, which is faster?

前几天我和一个朋友就那两个片段争论不休。哪个更快,为什么?

value = 5;
if (condition) {
    value = 6;
}

和:

if (condition) {
    value = 6;
} else {
    value = 5;
}

如果 value 是矩阵呢?

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

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

在未优化的代码中,第一个示例总是分配一个变量一次,有时两次。第二个例子只给一个变量赋值一次。条件在两个代码路径上都是相同的,所以这无关紧要。在优化代码中,它取决于编译器。

一如既往,如果您担心,请生成程序集并查看编译器实际在做什么。

TL;DR: 在未优化的代码中,没有 elseif 似乎无关紧要地提高了效率,但即使是最基本的优化级别也启用了代码基本上改写为value = condition + 5.


gave it a try 并为以下代码生成程序集:

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;
}

在禁用优化的 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]

对于 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]

后者看起来效率稍低,因为它有一个额外的跳跃,但两者都有至少两个和最多三个任务,所以除非你真的需要挤压每一滴性能(提示:除非你正在处理 space 穿梭你不会,即使那样你 可能 也不会)差异不会很明显。

然而,即使使用最低的优化级别 (-O1),两个函数也会减少到相同的程度:

test    dil, dil
setne   al
movzx   eax, al
add     eax, 5

基本上等同于

return 5 + condition;

假设 condition 是零或一。 更高的优化级别并没有真正改变输出,除了它们通过在开始时有效地将 EAX 寄存器清零来设法避免 movzx


免责声明: 你可能不应该自己写 5 + condition(即使标准保证将 true 转换为整数类型会得到 1) 因为对于阅读你代码的人(可能包括你未来的自己)来说,你的意图可能不会立即显而易见。这段代码的目的是表明编译器在这两种情况下产生的结果(实际上)是相同的。 在评论中说得很好:

a human's job is to write code for humans and let the compiler write code for the machine.

的回答表明,对于 int,它们都针对同一程序集进行了优化,因此无关紧要。

What if value is a matrix ?

我将以更一般的方式解释这一点,即如果 value 是一种构造和赋值成本高(移动成本低)的类型。

然后

T value = init1;
if (condition)
   value = init2;

是次优的,因为如果 condition 为真,您将对 init1 进行不必要的初始化,然后进行复制分配。

T value;
if (condition)
   value = init2;
else
   value = init3;

这样更好。但是如果默认构造是昂贵的并且如果复制构造比初始化更昂贵,那么仍然不是最优的。

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

T value = condition ? init1 : init2;

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

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

T value = create(condition);

根据init1init2是什么你也可以这样考虑:

auto final_init = condition ? init1 : init2;
T value = final_init;

但我必须再次强调,只有当给定类型的构造和赋值非常昂贵时,这才有意义。即便如此,也只有通过 分析 你才能确定。

在伪汇编语言中,

    li    #0, r0
    test  r1
    beq   L1
    li    #1, r0
L1:

可能会也可能不会

    test  r1
    beq   L1
    li    #1, r0
    bra   L2
L1:
    li    #0, r0
L2:

取决于实际 CPU 的复杂程度。从最简单到最奇特:

  • any CPU 大约在 1990 年后制造,良好的性能取决于 instruction cache 内的代码拟合。因此,当有疑问时,尽量减少代码大小。这有利于第一个例子。

  • 有了基本的“in-order, five-stage pipeline" CPU, which is still roughly what you get in many microcontrollers, there is a pipeline bubble 每次分支——有条件的或无条件的——都被采用,所以尽量减少分支指令的数量也很重要。这也有利于第一个例子。

  • 稍微复杂一些 CPUs——花哨的足以做“out-of-order execution", but not fancy enough to use the best known implementations of that concept—may incur pipeline bubbles whenever they encounter write-after-write hazards。这有利于 second 的例子,其中 r0 无论如何只写一次。这些 CPU 通常足够花哨,可以在指令提取器中处理无条件分支,所以你 不是 用写后写惩罚换取分支惩罚。

    不知道现在还有没有人在做这种CPU。但是使用乱序执行的"best known implementations"的CPU很可能会在不常用的指令上偷工减料,所以需要要知道这种事情可能会发生。一个真实的例子是 false data dependencies on the destination registers in popcnt and lzcnt on Sandy Bridge CPUs.

  • 在最高端,OOO 引擎最终会为两个代码片段发出完全相同的内部操作序列——这是 "don't worry about it, the compiler will generate the same machine code either way." 的硬件版本但是,代码大小仍然确实很重要,现在您还应该担心条件分支的可预测性。 Branch prediction failures potentially cause a complete pipeline flush, which is catastrophic for performance; see Why is it faster to process a sorted array than an unsorted array? 了解这能带来多大的不同。

    如果分支 高度不可预测的,并且您的 CPU 有条件设置或条件移动指令,现在是使用它们的时候了:

        li    #0, r0
        test  r1
        setne r0
    

        li    #0, r0
        li    #1, r2
        test  r1
        movne r2, r0
    

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

是什么让您认为它们中的任何一个甚至是一个班轮更快或更慢?

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);
}

高级语言的代码行越多,编译器的工作量就越大,因此如果您想对其制定通用规则,请为编译器提供更多的代码。如果算法与上述情况相同,那么人们会期望编译器进行最少的优化来解决这个问题。

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

虽然第一个函数的执行顺序不同,但执行时间相同,这不足为奇。

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

希望你明白如果不同的实现实际上并没有不同不是很明显的话你可以尝试这个。

就矩阵而言,不确定它有多重要,

if(condition)
{
 big blob of code a
}
else
{
 big blob of code b
}

只是将相同的 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

我们最近刚刚在 Whosebug 上与其他人完成了这个练习。有趣的是,在那种情况下,这个 mips 编译器不仅意识到函数是相同的,而且让一个函数简单地跳转到另一个函数以节省代码 space。不过这里没有这样做

00000000 <fun0>:
   0:   0004102b    sltu    ,[=15=],
   4:   03e00008    jr  
   8:   24420005    addiu   ,,5

0000000c <fun1>:
   c:   0004102b    sltu    ,[=15=],
  10:   03e00008    jr  
  14:   24420005    addiu   ,,5

00000018 <fun2>:
  18:   0004102b    sltu    ,[=15=],
  1c:   03e00008    jr  
  20:   24420005    addiu   ,,5

更多目标。

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 , r0
   e:   1585            mov (sp)+, r5
  10:   0087            rts pc
  12:   15c0 0005       mov , 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 , r0
  28:   1585            mov (sp)+, r5
  2a:   0087            rts pc
  2c:   15c0 0006       mov , 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 , r0
  42:   1585            mov (sp)+, r5
  44:   0087            rts pc
  46:   15c0 0006       mov , 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

和编译器

有了这个我的代码,人们希望不同的目标也能匹配

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

现在从技术上讲,这些解决方案中的一些解决方案存在性能差异,有时结果是 5 个 case 跳过结果是 6 个代码,反之亦然,分支比执行更快吗?有人可能会争论,但执行应该有所不同。但这更像是代码中的 if 条件与 if not 条件,导致编译器执行 if this jump over else execute through。但这不一定是由于编码风格,而是比较以及 if 和 else 情况下的任何语法。

好的,既然汇编是标签之一,我就假设你的代码是伪代码(不一定是 c),并由人工将其翻译成 6502 汇编。

第一个选项(没有其他)

        ldy #[=10=]
        lda #
        dey
        bmi false
        lda #
false   brk

第二个选项(还有其他)

        ldy #[=11=]
        dey
        bmi else
        lda #
        sec
        bcs end
else    lda #
end     brk

假设:条件在 Y 寄存器中,在任一选项的第一行将其设置为 0 或 1,结果将在累加器中。

所以,在计算了每种情况的两种可能性的循环之后,我们发现第一个构造通常更快;条件为 0 时为 9 个周期,条件为 1 时为 10 个周期,而选项二在条件为 0 时也是 9 个周期,但条件为 1 时为 13 个周期。(周期计数不包括 BRK 最后).

结论:If onlyIf-Else 构造更快。

为了完整起见,这里有一个优化的 value = condition + 5 解决方案:

ldy #[=12=]
lda #[=12=]
tya
adc #
brk

这将我们的时间减少到 8 个周期(同样不包括最后的 BRK)。