【问题标题】:If statement vs if-else statement, which is faster?if 语句与 if-else 语句,哪个更快?
【发布时间】:2017-08-29 08:17:49
【问题描述】:

前几天我和一个朋友就这两个 sn-ps 发生了争执。哪个更快,为什么?

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

和:

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

如果value 是一个矩阵呢?

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

编辑(由工作人员要求,因为问题目前处于搁置状态):

  • 请考虑由优化和非优化版本的主流编译器(如 g++、clang++、vc、mingw)或 MIPS 生成的 x86 程序集 来回答大会
  • 当程序集不同时,解释为什么版本更快以及何时(例如“更好,因为没有分支和分支有以下问题等等”

【问题讨论】:

  • 优化会扼杀这一切...没关系...
  • 您可以对其进行分析,我个人怀疑您使用现代编译器会发现任何差异。
  • 使用value = condition ? 6 : 5; 而不是if/else 很可能会生成相同的代码。如果您想了解更多信息,请查看汇编输出。
  • 这种情况下最重要的是避开分支,这是这里最昂贵的东西。 (管道重新加载,丢弃预取指令等)
  • 唯一一次像这样对速度进行微优化是有意义的,它是在一个将运行很多次的循环中,并且优化器可以优化所有分支指令,就像 gcc 一样这个微不足道的例子或现实世界的性能将在很大程度上取决于正确的分支预测(必须链接到*.com/questions/11227809/…)。如果您不可避免地会在循环中分支,您可以通过生成配置文件并使用它重新编译来帮助分支预测器。

标签: c++ performance c++11 assembly microbenchmark


【解决方案1】:

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]

后者看起来效率略低,因为它有一个额外的跳跃,但两者都至少有两个和最多三个任务,所以除非你真的需要挤出最后一滴性能(提示:除非你在航天飞机上工作不要,即使那样你可能不要)差异不会很明显。

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

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

基本上相当于

return 5 + condition;

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


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

人类的工作是为人类编写代码,并让编译器机器编写代码>。

【讨论】:

  • 这是一个很好的答案,应该被接受。
  • 我从来没有想过使用加法(
  • @CiprianTomoiaga 除非您正在编写优化器,否则您不应该这样做!在几乎所有情况下,您都应该让编译器进行这样的优化,尤其是在它们严重降低代码可读性的情况下。只有当性能测试显示某段代码存在问题时,您才应该开始尝试对其进行优化,即使如此,也要保持其整洁和良好的注释,并且只执行能够产生可衡量差异的优化。
  • 我想回复 Muzer,但它不会在线程中添加任何内容。但是,我只想重申人类的工作是为人类编写代码并让编译器编写代码机器。我是从编译器开发人员 PoV 那里说的(我不是,但我对它们有所了解)
  • The value true converted to int always yields 1, period. 当然,如果你的条件仅仅是“真实”,而不是booltrue,那就完全不同了。
【解决方案2】:

CompuChip 的回答表明,对于int,它们都针对同一个程序集进行了优化,所以没关系。

如果 value 是一个矩阵呢?

我将以更一般的方式解释这一点,即如果 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;

但我必须再次强调,这仅在给定类型的构造和赋值非常昂贵时才相关。即便如此,只有通过分析您才能确定。

【讨论】:

  • 昂贵的未优化。例如,如果默认构造函数将矩阵清零,编译器可以意识到赋值只是要覆盖那些 0,所以根本不将其清零,而是直接写入这个内存。当然,优化器是挑剔的野兽,因此很难预测它们何时会启动...
  • @MatthieuM。当然。我所说的“昂贵”是指“即使在编译器优化之后,执行起来也很昂贵(按指标,无论是 CPU 时钟、资源利用率等)。
  • 在我看来,默认结构不太可能很昂贵,但移动起来很便宜。
  • @plugwash 考虑一个具有非常大的分配数组的类。默认构造函数分配和初始化数组,这是昂贵的。移动(不是复制!)构造函数只需与源对象交换指针,不需要分配或初始化大数组。
  • 只要部分简单,我肯定更喜欢使用?: 运算符而不是引入新功能。毕竟,您可能不仅会将条件传递给函数,还会传递一些构造函数参数。其中一些甚至可能不会被create() 使用,具体取决于条件。
【解决方案3】:

在伪汇编语言中,

    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 的复杂程度。从最简单到最精美:

  • 对于大约 1990 年之后制造的 任何 CPU,良好的性能取决于 instruction cache 中的代码。因此,如有疑问,请尽量减少代码大小。这有利于第一个示例。

  • 使用基本的“in-order, five-stage pipeline”CPU,这在许多微控制器中仍然大致是这样,每次执行分支(条件或无条件)时都会有一个 pipeline bubble,因此它也很重要尽量减少分支指令的数量。这也有利于第一个示例。

  • 更复杂的 CPU — 足够花哨来执行“out-of-order execution”,但不够花哨以使用该概念的最知名实现 — 遇到write-after-write hazards 时可能会引发管道气泡。这有利于 second 示例,其中r0 无论如何只写入一次。这些 CPU 通常足以处理指令提取器中的无条件分支,因此您不会只是用写后写惩罚来换取分支惩罚。

    我不知道现在还有没有人在制造这种 CPU。但是,确实使用乱序执行的“最知名的实现”的 CPU 可能会在不常用的指令上偷工减料,因此您需要注意这类事情可以发生。一个真实的例子是false data dependencies on the destination registers in popcnt and lzcnt on Sandy Bridge CPUs

  • 在最高端,OOO 引擎最终会为两个代码片段发出完全相同的内部操作序列——这是硬件版本的“别担心,编译器也会生成相同的机器码”大大地。”但是,代码大小仍然很重要,现在您还应该担心条件分支的可预测性。 Branch prediction 失败可能会导致完整的管道flush,这对性能来说是灾难性的;请参阅 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指令的调度和执行资源;如果分支实际上是可预测的,那么分支版本可能会更快。

【讨论】:

  • 我将重新表述您的第二点,即 CPU 是否有一个因写后写危险而延迟的无序引擎。如果 CPU 有一个乱序引擎可以毫不拖延地处理这些危险,那没有问题,但如果 CPU 根本没有乱序引擎也没有问题>.
  • @supercat 最后的段落是为了涵盖这种情况,但我会考虑如何使其更清晰。
  • 我不知道当前的 CPU 有哪些缓存,这会导致第二次按顺序执行的代码比第一次运行得更快(一些基于闪存的 ARM 部件具有可以缓冲的接口几行闪存数据,但可以像执行它一样快地按顺序获取代码,但是使分支繁重的代码在这些数据上快速运行的关键是将其复制到 RAM 中)。完全没有任何乱序执行的 CPU 远比那些会因 write-after-write 危害而延迟的 CPU 更为常见。
  • 这个很有见地
【解决方案4】:

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

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

【讨论】:

  • 如果关心性能,那么他们不会在未优化的情况下编译。但当然,优化器有多“好”取决于编译器/版本。
  • AFAIK 没有关于哪个编译器/CPU 架构等的评论,因此他们的编译器可能不会进行优化。他们可以在任何东西上编译,从 8 位 PIC 到 64 位 Xeon。
【解决方案5】:

是什么让你认为它们中的任何一个,即使是一个班轮更快或更慢?

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 包装器放在大块代码周围,无论它们 value=5 还是更复杂的东西。同样,比较即使它是一大块代码,它仍然需要计算,并且等于或不等于某事通常用否定编译,如果(条件)做​​某事通常编译为 if not condition 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

我们最近在 * 上与其他人一起完成了这个练习。有趣的是,在这种情况下,这个 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

更多目标。

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

和编译器

有了这个 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

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

【讨论】:

    【解决方案6】:

    好的,由于程序集是标签之一,我将假设您的代码是伪代码(不一定是 c)并由人工将其翻译成 6502 程序集。

    第一个选项(没有其他)

            ldy #$00
            lda #$05
            dey
            bmi false
            lda #$06
    false   brk
    

    第二个选项(带其他)

            ldy #$00
            dey
            bmi else
            lda #$06
            sec
            bcs end
    else    lda #$05
    end     brk
    

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

    因此,在计算每种情况的两种可能性的周期后,我们看到第一个构造通常更快;条件为 0 时为 9 个周期,条件为 1 时为 10 个周期,而选项二在条件为 0 时也是 9 个周期,但条件为 1 时为 13 个周期。(循环计数不包括末尾的 BRK )。

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

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

    ldy #$00
    lda #$00
    tya
    adc #$05
    brk
    

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

    【讨论】:

    • 不幸的是,对于这个答案,将相同的源代码输入到 C 编译器(或输入到 C++ 编译器)产生的输出与输入到 Glen 的大脑中的输出大不相同。在源代码级别的任何替代方案之间没有区别,没有“优化”潜力。只需使用最易读的那个(大概是 if/else 那个)。
    • @是的。编译器可能会将 both 变体优化为最快的版本,或者可能会增加远远超过两者之间差异的额外开销。或两者兼而有之。
    • 假设它是“不一定是 C”似乎是一个明智的选择,因为问题被标记为 C++(不幸的是,它没有设法声明 类型所涉及的变量)。