是if else比if+default更快
Is if else faster than if + default?
我做了一个简单的实验,将if-else与仅if(预设默认值)进行比较。示例:
void test0(char c, int *x) {
*x = 0;
if (c == 99) {
*x = 15;
}
}
void test1(char c, int *x) {
if (c == 99) {
*x = 15;
} else {
*x = 0;
}
}
对于上面的函数,我得到了完全相同的汇编代码(使用cmovne
)。
但是,当添加一个额外的变量时:
void test2(char c, int *x, int *y) {
*x = 0;
*y = 0;
if (c == 99) {
*x = 15;
*y = 21;
}
}
void test3(char c, int *x, int *y) {
if (c == 99) {
*x = 15;
*y = 21;
} else {
*x = 0;
*y = 0;
}
}
组装突然变得不同:
test2(char, int*, int*):
cmp dil, 99
mov DWORD PTR [rsi], 0
mov DWORD PTR [rdx], 0
je .L10
rep ret
.L10:
mov DWORD PTR [rsi], 15
mov DWORD PTR [rdx], 21
ret
test3(char, int*, int*):
cmp dil, 99
je .L14
mov DWORD PTR [rsi], 0
mov DWORD PTR [rdx], 0
ret
.L14:
mov DWORD PTR [rsi], 15
mov DWORD PTR [rdx], 21
ret
似乎唯一的区别是顶部的mov
是在je
之前还是之后完成的。
现在(很抱歉,我的程序集有点粗糙),为了节省管道刷新,在跳转后使用mov
不是总是更好吗?如果是这样,优化器(gcc6.2-O3)为什么不使用更好的方法呢?
当然,有些编译器可能会进行优化,但这并不能保证。对于这两种编写函数的方法,您很可能会得到不同的目标代码。
事实上,没有优化是有保证的(尽管现代优化编译器在大多数时候都做得很好),所以你应该编写代码来捕捉你想要的语义,或者你应该验证生成的目标代码并编写代码来确保你得到预期的输出。
以下是旧版本的MSVC在瞄准x86-32时将生成的内容(主要是因为他们不知道使用CMOV指令):
test0 PROC
cmp BYTE PTR [c], 99
mov eax, DWORD PTR [x]
mov DWORD PTR [eax], 0
jne SHORT LN2
mov DWORD PTR [eax], 15
LN2:
ret 0
test0 ENDP
test1 PROC
mov eax, DWORD PTR [x]
xor ecx, ecx
cmp BYTE PTR [c], 99
setne cl
dec ecx
and ecx, 15
mov DWORD PTR [eax], ecx
ret 0
test1 ENDP
请注意,test1
为您提供了无分支代码,该代码利用SETNE
指令(一个条件集,它将根据条件代码将其操作数设置为0或1——在本例中为NE
)以及一些位操作来产生正确的值。CCD_ 8使用条件分支跳过对CCD_ 9的15的分配。
这之所以有趣,是因为它几乎与你所期望的完全相反。天真地说,人们可能会认为test0
将是你握住优化器的手并让它生成无分支代码的方式。至少,这是我脑海中的第一个想法。但事实并非如此!优化器能够识别if
/else
习语并进行相应的优化!在test0
的情况下,它无法进行同样的优化,因为你试图智胜它
但是,当添加额外的变量时。。。组件突然变成不同的
好吧,这并不奇怪。代码中的微小更改通常会对发出的代码产生重大影响。优化者不是魔法;它们只是非常复杂的模式匹配器。你改变了模式!
诚然,优化编译器可以在这里使用两个条件移动来生成无分支代码。事实上,这正是Clang 3.9对test3
所做的(但对test2
则不然,这与我们上面的分析一致,即优化器可能比不寻常的模式更能识别标准模式)。但海湾合作委员会不这么做。同样,不能保证执行特定的优化。
似乎唯一的区别是顶部的"mov"是在"je"之前还是之后完成的。
现在(很抱歉,我的程序集有点粗糙),为了节省管道冲洗,在跳转后使用mov不是总是更好吗?
不,不是真的。在这种情况下,这不会改善代码。如果分支预测错误,不管怎样,都将进行管道刷新。推测性预测错误的代码是ret
指令还是mov
指令并不重要。
ret
指令紧跟在条件分支之后的唯一原因是,如果您手动编写程序集代码,而不知道使用rep ret
指令。这对于某些AMD处理器来说是必要的技巧,可以避免分支预测惩罚。除非你是一位组装大师,否则你可能不会知道这个技巧。但编译器确实如此,而且也知道当你专门针对没有这种怪癖的英特尔处理器或不同一代AMD处理器时,这是没有必要的。
然而,您可能是对的,最好在分支之后使用mov
,但不是出于您建议的原因。现代处理器(我相信这是Nehalem和更高版本,但如果需要验证的话,我会在Agner Fog的优秀优化指南中查找它)能够在某些情况下进行宏操作融合。基本上,宏操作融合意味着CPU的解码器将两个符合条件的指令组合成一个微操作,从而在流水线的各个阶段节省带宽。cmp
或test
指令后面跟着条件分支指令(如test3
中所示)符合宏操作融合的条件(实际上,还必须满足其他条件,但此代码确实满足这些要求)。在cmp
和je
之间调度其他指令,如您在test2
中看到的,会使宏操作融合变得不可能,可能会使代码执行得更慢。
不过,可以说,这是编译器中的一个优化缺陷。它可以重新排序mov
指令,将je
放在cmp
之后,保留宏操作融合的能力:
test2a(char, int*, int*):
mov DWORD PTR [rsi], 0 ; do the default initialization *first*
mov DWORD PTR [rdx], 0
cmp dil, 99 ; this is now followed immediately by the conditional
je .L10 ; branch, making macro-op fusion possible
rep ret
.L10:
mov DWORD PTR [rsi], 15
mov DWORD PTR [rdx], 21
ret
CCD_ 30和CCD_ 31的目标代码之间的另一个区别是代码大小。由于优化器发出填充以对齐分支目标,test3
的代码比test2
大4个字节。不过,这不太可能有足够的区别,尤其是如果这段代码不是在保证在缓存中处于热状态的紧密循环中执行的。
那么,这是否意味着您应该始终像在test2
中那样编写代码
嗯,不,有几个原因:
- 正如我们所看到的,它可能是一个令人讨厌的东西,因为优化器可能无法识别模式
- 您应该首先为可读性和语义正确性编写代码,只有当您的探查器指示它实际上是一个瓶颈时,才返回进行优化。然后,您应该只在检查和验证编译器发出的目标代码后进行优化,否则可能会出现令人讨厌的结果。(标准的"在得到证明之前,请相信您的编译器"建议。)
尽管在某些非常简单的情况下它可能是最优的,但"预设"习语是不可推广的。如果初始化很耗时,那么在可能的情况下跳过它可能会更快。(这里讨论了一个例子,在VB6的上下文中,字符串操作非常慢,在可能的情况下删除它实际上会比花哨的无分支代码更快地执行时间。更普遍地说,如果你能够围绕函数调用进行分支,同样的原理也适用。)
即使在这里,它似乎会产生非常简单且可能更优化的代码,但实际上它可能会更慢,因为在
c
等于99的情况下,您要向内存写入两次,而在c
不等于99的情况下,您什么都不保存。您可以通过重写代码来节省此成本,使其将最终值累积在临时寄存器中,只在最后将其存储到内存中,例如:
test2b(char, int*, int*): xor eax, eax ; pre-zero the EAX register xor ecx, ecx ; pre-zero the ECX register cmp dil, 99 je Done mov eax, 15 ; change the value in EAX if necessary mov ecx, 21 ; change the value in ECX if necessary Done: mov DWORD PTR [rsi], eax ; store our final temp values to memory mov DWORD PTR [rdx], ecx ret
但这会破坏两个附加寄存器(
eax
和ecx
),并且实际上可能不会更快。你必须对它进行基准测试。或者相信编译器会在它实际上是最优的时候发出这个代码,比如当它在一个紧密循环中内联了一个像test2
这样的函数时。即使您可以保证以某种方式编写代码会导致编译器发出无分支代码,这也不一定会更快!虽然分支在预测失误时速度较慢,但预测失误实际上相当罕见。现代处理器具有非常良好的分支预测引擎,在大多数情况下实现超过99%的预测准确率。
条件移动对于避免分支预测失误非常有用,但它们的重要缺点是增加了依赖链的长度。相反,正确预测的分支会破坏依赖链。(这可能是GCC在添加额外变量时不发出两条CMOV指令的原因。)如果预计分支预测失败,则条件移动只是性能上的胜利。如果你可以指望预测成功率达到约75%或更好,那么条件分支可能更快,因为它打破了依赖链,延迟更低。我怀疑这里会是这样,除非每次调用函数时
c
在99和not-99之间快速来回切换。(参见Agner Fog的"优化汇编语言中的子程序",第70–71页。)
- 学习多线程C++:添加线程不会使执行速度更快,即使它看起来应该
- 二叉搜索如何比线性搜索更快?
- push_back并插入 C++ STL 中哪个更快?
- 如何使插入排序更快?
- C++,为什么数组比矢量更快,使用更少的内存
- 哪个更快:在 1d 向量中按字符串搜索还是在 2d 向量中按向量搜索?
- 哪种方式更快?究竟发生了什么,我们没有看到什么?
- if 语句与 if-else 语句,哪个更快
- 为什么if语句和变量声明要比循环中的添加更快
- 是if else比if+default更快
- 在C++中使用位运算符还是if语句更快
- 如果没有if,std::count_if会更快吗
- 使用虚拟函数代替IF语句会更快
- 更快地创建指针映射或if语句
- c++更快的if else语句
- 哪一个更快?函数调用或条件if语句
- 是否有更快/更短的方法在C++而不是使用"if"进行这些测试?
- 在c++ 11中,哪个更正确、更快:switch-case还是if().| |.[|].
- 在C语言中,如果if语句包含赋值语句,是否会更快?
- 哪个更快:if(bool)还是if(int)