如果语句与if-else语句,哪个更快?

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

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

和:

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

如果value是一个matrix呢?

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

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

  • 请考虑由主stream编译器(如g ++,clang ++,vc,mingw )在优化和非优化版本或MIPS汇编中生成的x86 汇编
  • 当大会不同时,解释为什么一个版本更快,何时( 例如“更好,因为没有分支和分支有下列问题blahblah”

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] 

后者看起来效率稍低,因为它有一个额外的跳跃,但都至less有两个,最多三个任务,所以除非你真的需要挤压每一滴performance(提示:除非你在航天飞机上工作,否则你不,即使这样你可能不会)差异不会显而易见。

但是,即使在最低优化级别( -O1 )下,这两个函数也会降低到相同的水平:

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

这基本上相当于

 return 5 + condition; 

假设condition是零或一个。 更高的优化级别不会真正改变输出,除非他们在movzx通过有效地清零EAX寄存器来避免movzx


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

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

CompuChip的答案显示,对于int它们都针对相同的程序集进行了优化,所以没关系。

如果价值是一个matrix呢?

我将以更一般的方式解释这个问题,即如果value是一种结构和分配都很昂贵(而且价格便宜)的types。

然后

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

是次优的,因为在condition为真的情况condition ,你对init1做了不必要的初始化,然后你做了复制分配。

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

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

你有条件运营商的解决scheme是很好的:

 T value = condition ? init1 : init2; 

但是我会主张创build一个这样的帮助函数:

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

但是,我必须强调,只有在施工和任务对于给定types来说非常昂贵的情况下,这才是有意义的。 即使如此,只有通过分析你肯定知道。

在未优化的代码中,第一个例子总是分配一个variables,有时两次。 第二个例子只分配一次variables。 条件在两个代码path上是相同的,所以这应该不重要。 在优化的代码中,这取决于编译器。

像往常一样,如果你是这个问题,生成程序集,看看编译器实际上在做什么。

在伪汇编语言中,

  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,良好的性能取决于指令caching内的代码。 因此,如果有疑问,请最小化代码大小。 这有利于第一个例子。

  • 有了一个基本的“ 五阶段stream水线 ”的CPU,它仍然是您在许多微控制器中所获得的大致结果,每当采用分支条件或无条件分支时,都会出现stream水线泡泡 ,因此减less分支指令的数量。 这也有利于第一个例子。

  • 稍微复杂一点的CPU–足以做“ 无序执行 ”,但不够花式使用该概念的最佳已知实现 – 可能会在遇到写后写后备(write-after-write)危险时产生pipe道气泡。 这有利于第二个例子,其中r0只写一次,无论如何。 这些CPU通常足以处理指令获取器中的无条件分支,所以你不仅仅为分支惩罚交易写后写惩罚。

    我不知道是否有人还在制造这种CPU。 但是,使用乱序执行的“最知名的实现”的CPU很可能会偷偷使用不太常用的指令,所以您需要意识到这种事情可能会发生。 一个真实的例子是在Sandy Bridge CPU上的popcntlzcnt的目标寄存器上存在错误的数据依赖性 。

  • 在最高端,OOO引擎将发出完全相同的两个代码片段的内部操作序列 – 这是硬件版本的“不用担心它,编译器将生成相同的机器代码”。 但是,代码大小仍然是重要的,现在你也应该担心条件分支的可预测性。 分支预测失败可能导致完整的pipe线冲洗 ,这对性能是灾难性的; 请参阅为什么处理sorting的数组比未sorting的数组更快? 了解这可以造成多大的差异。

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

      li #0, r0 test r1 setne r0 

    要么

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

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

什么会让你觉得他们中的任何一个,甚至是一个class轮是更快还是更慢?

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

高级语言的更多行代码给了编译器更多的工作,所以如果你想制定一个通用的规则,给编译器更多的代码工作。 如果algorithm与上面的情况相同,那么可以期望编译器使用最less的优化来解决这个问题。

 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 

不是一个大惊喜,它做了不同的顺序,相同的执行时间,虽然第一个function。

 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 

希望你能得到这个想法,如果不是很明显,不同的实现实际上并没有什么不同,那么你可以试试这个想法。

就matrix而言,不知道这很重要,

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

只是将相同的if-then-else包装放在代码的大块中,不pipe它们是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 

我们刚刚和其他人一起通过了这个练习。 有趣的是这种编译器在这种情况下不仅实现了function相同,而且有一个function只是跳转到另一个以节省代码空间。 虽然没有这样做

 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 

现在技术上在这些解决scheme中有一些性能上的差异,有时结果是5个案例跳过了6个代码的结果,反之亦然,是比通过执行更快的一个分支? 人们可以争辩,但执行应该会有所不同。 但是这更像是if条件,如果没有条件,代码会导致编译器执行,如果这个跳过其他执行通过。 但这不一定是由于编码风格,而是以任何语法进行比较以及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个周期,而条件为0时,选项2也为9个周期,条件为1时为13个周期( 周期计数不包括最终的BRK )。

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

为了完整性,这是一个优化的value = condition + 5解决scheme:

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

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

if语句稍微快一点,因为当我们将这个代码转换成汇编语言时, if else需要额外的跳转,而if有赋值。 跳转需要额外的时间,而分配速度很快。