if 语句与 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;
存在,我希望它更快,但这不是一个选项。
编辑(工作人员要求,因为问题目前被搁置):
- 请通过考虑主流编译器(例如G++,CL++,VC,MINGW)在优化和非优化版本中生成的x86程序集或MIPS程序集来回答。
- 当汇编不同时,解释为什么版本更快以及何时更快(例如,"更好,因为没有分支和分支有以下问题等等")
TL;DR:在未优化的代码中,没有else
if
似乎无关紧要地更有效,但即使是最基本的优化级别,代码也基本上被重写为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);
根据init1
和init2
,您还可以考虑这一点:
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 only
比If-Else
构造更快。
为了完整起见,这里有一个优化的value = condition + 5
解决方案:
ldy #$00
lda #$00
tya
adc #$05
brk
这将我们的时间减少到 8 个周期(同样不包括最后的BRK
)。
- 我的简单if-else语句是如何无法访问的代码
- 我似乎对if/else的基本语句有问题:/
- C++ If/Else 语句被跳过
- 如果条件不相关,我应该更喜欢两个 if 语句而不是 if-else 语句吗?
- 如何将 if else 语句重写为 switch 语句
- 在 C++ 中的 if-else if- else 语句期间更改变量
- 为什么我的 if else 语句不起作用并从数组中输出正确的索引?
- 我的 If Else 语句无法在向量 (C++) 中提供最大值
- 为什么无论输入如何,所有 if-else 语句都会打印?
- 为什么切换 if else 语句的顺序会导致错误?
- 如何使用 if else 语句
- if/else 语句输出由于加号或减号而未显示正确的消息
- if-else 语句仅按特定顺序工作,我不知道为什么
- "if else"被视为单个语句吗?
- 试图避免在 for 循环中出现 if-else 语句,但代码似乎有一些错误
- 如何在 if-else 语句中使用 C++20 的可能/不太可能属性
- While循环和if/else语句工作不正常
- 我的fahrenheit-celcius程序会忽略我的if-else语句,并在每次运行该程序时将值更改为0
- 如何添加语句if-else
- if语句-if/else和参数放置C++