else语句,哪个更快?
有一天我和一位朋友争论了这两个片段。 哪个更快,为什么?
value = 5;
if (condition) {
value = 6;
}
和:
if (condition) {
value = 6;
} else {
value = 5;
}
如果value
是一个矩阵呢?
注意:我知道value = condition ? 6 : 5;
value = condition ? 6 : 5;
存在,我预计它会更快,但它不是一种选择。
编辑 (由于问题暂时搁置,工作人员请求):
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
寄存器来避免movzx
。
免责声明:你可能不应该自己写5 + condition
(即使标准保证将true
的类型转换为1
),因为你的意图对读取你的代码的人(可能包括你未来的自己)来说可能不会立即显而易见。 这段代码的目的是为了说明编译器在两种情况下产生的结果(实际上)是相同的。 Ciprian Tomoiaga在评论中说得很好:
一个人的工作就是为人类编写代码,让编译器为机器编写代码。
CompuChip的答案显示,对于int
它们都针对相同的程序集进行了优化,所以没关系。
如果价值是一个矩阵呢?
我将以更一般的方式来解释这个问题,即如果value
是一种结构和任务昂贵(而且行动便宜)的类型。
然后
T value = init1;
if (condition)
value = init2;
是次优的,因为在condition
为真的情况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);
根据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很可能会偷偷使用不太常用的指令,所以您需要意识到这种事情可能会发生。 一个真实的例子是在Sandy Bridge CPU上的popcnt
和lzcnt
的目标寄存器上存在错误的数据依赖关系。
在最高端,OOO引擎将发布完全相同的两个代码片段的内部操作序列 - 这是“不用担心它的硬件版本,编译器将以任何方式生成相同的机器代码”。 但是,代码大小仍然很重要,现在您还应该担心条件分支的可预测性。 分支预测失败可能会导致完整的管道刷新,这对性能来说是灾难性的; 请参阅为什么处理排序的数组比处理未排序的数组更快? 了解这可以造成多大的差异。
如果分支高度不可预测,并且您的CPU具有条件设置或条件移动指令,则应该使用它们:
li #0, r0
test r1
setne r0
要么
li #0, r0
li #1, r2
test r1
movne r2, r0
条件集版本比任何其他替代方案都更紧凑; 如果该指令可用,那么即使该分支是可预测的,实际上也可以保证该方案是正确的。 条件移动版本需要额外的临时寄存器,并且总是浪费一条li
指令的调度和执行资源; 如果分支实际上是可预测的,分支版本可能会更快。