if 语句与 if-else 语句,哪个更快

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

本文关键字:语句 if-else if      更新时间:2023-10-16

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

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

和:

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

如果value是一个矩阵呢?

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

编辑(工作人员要求,因为问题目前被搁置):

  • 请通过考虑主流编译器(例如G++,CL++,VC,MINGW)在优化和非优化版本中生成的x86程序集或MIPS程序集来回答。
  • 当汇编不同时,解释为什么版本更快以及何时更快(例如,"更好,因为没有分支和分支有以下问题等等")

TL;DR:在未优化的代码中,没有elseif似乎无关紧要地更有效,但即使是最基本的优化级别,代码也基本上被重写为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;
}

在禁用优化的 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在评论中说得很好:

人类

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

CompuChip 的答案表明,对于int,它们都针对同一程序集进行了优化,因此没关系。

如果值是矩阵怎么办?

我将以更一般的方式解释这一点,即如果value属于构造和赋值昂贵的类型(并且移动很便宜)怎么办。

然后

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

是次优的,因为如果condition为 true,则执行不必要的初始化以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 的复杂程度。 从最简单到最华丽:

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

  • 对于基本的"有序五级流水线"CPU,这仍然是许多微控制器中大致得到的,每次获取分支(有条件或无条件)时都会出现流水线气泡,因此最小化分支指令的数量也很重要。 这也有利于第一个例子。

  • 稍微复杂的 CPU(足够花哨地执行
  • "乱序执行",但不够花哨地使用该概念的最知名实现)在遇到写后写危险时可能会产生管道气泡。 这有利于第二个示例,其中无论如何r0只写入一次。 这些 CPU 通常足够花哨,可以在指令提取器中处理无条件的分支,因此您不只是将写后写入惩罚换取分支惩罚。

    我不知道是否有人还在制造这种CPU。 但是,使用无序执行的"最知名实现"的 CPU 可能会在不太常用的指令上偷工减料,因此您需要注意这种事情可能会发生。 一个真实的例子是popcnt中目标寄存器上的错误数据依赖和 Sandy Bridge CPU 上的lzcnt

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

    如果分支高度不可预测,并且您的 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 包装器,无论它们是 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

和编译器

有了这个 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 不是条件,导致编译器执行 if 这种跳转 else 执行。 但这不一定是由于编码风格,而是由于比较以及任何语法中的 if 和 else 情况。

好的,由于汇编是标签之一,我只假设您的代码是伪代码(不一定是 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 个周期,而选项 2 在条件为 0 时也是 9 个周期,但在条件为 1 时为 13 个周期。(周期计数不包括末尾的BRK)。

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

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

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

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