if语句vs if-else语句,哪个更快?[关闭]


83

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

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

和:

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

如果value是矩阵怎么办?

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

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

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

173
优化将杀死所有这些……没关系……
量子物理学家

21
您可以对其进行概要分析,个人而言,我怀疑您会发现使用现代编译器会有所不同。
乔治

25
使用value = condition ? 6 : 5;代替极有if/else可能导致生成相同的代码。如果您想了解更多信息,请查看程序集输出。
Jabberwocky

8
在这种情况下,最重要的事情是避免分支,这是这里最昂贵的事情。(管道重新加载,丢弃预取的指令等)
Tommylee2k

11
对速度进行微优化是有意义的唯一一次,因为这样的循环会运行很多次,或者优化器可以优化所有分支指令(如gcc可以在这个平凡的示例中使用),也可以是真实世界性能将在很大程度上取决于正确的分支预测(必须链接到stackoverflow.com/questions/11227809/…)。如果您不可避免地要在循环内进行分支,则可以通过生成配置文件并对其进行重新编译来帮助分支预测器。
戴维斯洛

Answers:


281

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

在禁用优化功能(-O0)的gcc 6.3上,相关差异为:

 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为零或一。更高的优化级别并没有真正改变输出,除非它们设法movzx通过EAX在开始时有效地将寄存器清零来避免这种情况。


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

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


50
这是一个很好的答案,应该接受。
dtell '17

10
我永远不会使用加法(<
-python

26
@CiprianTomoiaga,除非您正在编写优化程序,否则您不应该这样做!在几乎所有情况下,您都应让编译器进行这种优化,尤其是在它们严重降低代码可读性的情况下。仅当性能测试显示出某些代码问题时,您才应该开始尝试对其进行优化,甚至保持其清洁和注释良好,并且仅执行能够产生可测量差异的优化。
Muzer

22
我想回复Muzer,但它不会在线程中添加任何内容。但是,我只想重申人类的工作是为人类编写代码然后让编译器机器编写代码。我说过的是从编译器开发人员PoV(不是,但我学到了一些有关知识)
CiprianTomoiagă17年

10
true转换为int始终为1的值。当然,如果您的条件仅仅是“真实的”而不是bool价值true,那将是另一回事。
TC

44

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

如果值是矩阵怎么办?

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

但是我必须再次强调,仅当构造和分配对于给定类型确实非常昂贵时,这才有意义。即使这样,也只能通过剖析才能确定。


3
昂贵不优化掉了。例如,如果默认的构造函数将矩阵清零,则编译器可能会意识到分配只是覆盖那些0,因此根本不将其清零,然后直接写入此内存。当然,优化器是挑剔的野兽,因此很难预测它们何时启动或不启动……
Matthieu

@MatthieuM。当然。我的意思是“昂贵的”是“即使在编译器优化后,执行的费用也很高(按度量标准,无论是CPU时钟,资源利用率等)
。– bolov

对我来说,默认构造似乎不太昂贵,但价格便宜。
plugwash

6
@plugwash考虑一个具有很大分配数组的类。默认构造函数会分配并初始化数组,这很昂贵。move(不是复制!)构造函数可以仅与源对象交换指针,而无需分配或初始化大数组。
TrentP

1
只要部件简单,我肯定会喜欢使用?:运算符而不是引入新功能。毕竟,您不仅可能将条件传递给函数,还会传递一些构造函数参数。根据情况,其中一些甚至可能无法使用create()
cmaster-恢复莫妮卡

12

用伪汇编语言

    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,良好的性能取决于指令高速缓存中的代码适合性。因此,如有疑问,请最小化代码大小。这有利于第一个示例。

  • 使用基本的“有序五级流水线” CPU(这仍然是许多微控制器所能获得的粗略的CPU),每次采用有条件或无条件分支时都会出现流水线气泡,因此最小化该点也很重要分支指令的数量。这也有利于第一个示例。

  • 某些更复杂的CPU(足以执行“乱序执行”,但不足以使用该概念的最著名实现)可能会在遇到写后写风险时引起管道气泡。这有利于第二个示例,r0无论如何,该示例仅写入一次。这些CPU通常足够花哨的时间来处理指令提取程序中的无条件分支,因此,您只是将写后写代价换成分支代价。

    我不知道是否有人仍在使用这种CPU。但是,确实使用乱序执行的“最佳已知实现”的CPU很有可能会减少不经常使用的指令的执行速度,因此您需要意识到这种事情会发生。一个真实的例子是Sandy Bridge CPUpopcnt和目标寄存器上错误的数据依赖关系lzcnt

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

    如果分支高度不可预测的,并且您的CPU具有条件设置或条件移动指令,则可以使用它们:

        li    #0, r0
        test  r1
        setne r0
    

    要么

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

    条件集版本也比任何其他替代方案都更紧凑。如果该指令可用,则即使分支是可预测的,实际上也可以保证是此情况的正确选择。条件移动版本需要一个额外的暂存器,并且总是浪费一条li指令的调度和执行资源。如果分支实际上是可预测的,则分支版本可能会更快。


关于CPU是否具有乱序引擎,该顺序引擎会因写后写的危险而延迟,我想再说一遍。如果CPU的乱序引擎可以毫不延迟地处理此类危险,那没有问题,但是如果CPU根本没有乱序引擎也没有问题。
超级猫

@supercat最后的段落旨在介绍这种情况,但我会考虑如何使其更清楚。
zwol

我不知道当前有哪些CPU具有缓存,缓存将导致顺序执行的代码第二次运行比第一次运行更快(某些基于闪存的ARM部件具有可以缓存几行闪存数据的接口,但是可以按与执行它们一样快的顺序顺序地获取代码,但是使重分支代码在其上快速运行的关键是将其复制到RAM)。根本没有任何乱序执行的CPU比写后写危险会延迟的CPU更为常见。
超级猫

这非常有见地
Julien__

9

在未优化的代码中,第一个示例始终将变量分配一次,有时两次。第二个示例只分配一次变量。两个代码路径上的条件都相同,所以没关系。在优化的代码中,它取决于编译器。

与往常一样,如果您担心的话,请生成程序集并查看编译器的实际操作。


1
如果担心性能,那么它们将不会进行优化编译。但可以肯定的是,优化器的“好”取决于编译器/版本。
old_timer

AFAIK尚无关于哪种编译器/ CPU架构等的评论,因此潜在地,其编译器不会进行优化。他们可能在从8位PIC到64位Xeon的任何内容上进行编译。
尼尔,

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

高级语言的更多代码行使编译器可以使用更多代码,因此,如果要制定通用规则,可以使编译器可以使用更多代码。如果算法与上述情况相同,则可以期望编译器以最小的优化来解决该问题。

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还是更复杂的东西。同样,即使比较是一大段代码,也必须计算该比较,并且等于或不等于某事时通常使用负数进行编译,如果(条件)做​​某事时经常将其编译为无条件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

我们最近刚刚在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

还有更多目标。

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

和编译器

与此我代码,人们会期望不同的目标也要匹配

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 if else over执行。但这不一定是由于编码风格引起的,而是因为比较以及无论采用哪种语法的if和else情况。


0

好的,因为汇编是标记之一,所以我只假设您的代码是伪代码(不一定是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,结果将在累加器中。

因此,在计算完每种情况的两种可能性的周期后,我们看到第一种构造通常更快;9个周期时的条件是0和10个周期时的条件是1,而方案(2)也9个周期时的条件是0,但是,当条件是1. 13个循环(循环计数不包括BRK在端部)。

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

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

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

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


6
不幸的是,对于此答案,将相同的源代码输入到C编译器(或C ++编译器)中所产生的输出与将其输入到Glen的大脑中截然不同。在源代码级别上,任何替代方案之间都没有差异,也没有“优化”潜力。只需使用可读性最高的一种(大概是if / else一种)。
Quuxplusone

1
是的 编译器可能会将这两种变体优化到最快的版本,或者可能会增加远远超过两者之间的差异的额外开销。或两者。
jpaugh

1
假定问题被标记为C ++,假设它不是C ”确实是明智的选择(不幸的是,它无法声明所涉及变量的类型)。
Toby Speight
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.