我进行了一个简单的实验,将 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
s 在 je
之前或之后完成.现在(对不起,我的组装有点粗糙),拥有
mov
不是总是更好吗? s 跳转后,为了节省管道刷新?如果是这样,为什么优化器(gcc6.2 -O3)不使用更好的方法?
最佳答案
For the functions above, I got the exact same assembly code (using cmovne).
当然,一些编译器可能会进行这种优化,但不能保证。您很可能会为这两种编写函数的方式获得不同的目标代码。
事实上,没有优化是保证的(尽管现代优化编译器在大多数情况下都做得很好),所以你应该编写代码来捕获你想要的语义,或者你应该验证生成的目标代码和编写代码以确保获得预期的输出。
以下是旧版本的 MSVC 在针对 x86-32(主要是 because they don't know to use the CMOV instruction )时将生成的内容:
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
)结合一些位操作以产生正确的值。 test0
使用条件分支跳过将 15 赋值给 *x
.这很有趣的原因是因为它几乎与您所期望的完全相反。天真地,人们可能会期望
test0
将是您握住优化器的手并让它生成无分支代码的方式。至少,这是我脑海中闪过的第一个念头。但事实上,事实并非如此!优化器能够识别 if
/else
成语并相应优化!在 test0
的情况下无法进行相同的优化,你试图超越它。However when adding an extra variable ... The assembly suddenly becomes different
好吧,这并不奇怪。代码中的微小更改通常会对发出的代码产生重大影响。优化器不是魔术;它们只是非常复杂的模式匹配器。你改变了模式!
当然,优化编译器可以在这里使用两个条件移动来生成无分支代码。事实上,这正是 Clang 3.9 为
test3
所做的。 (但不适用于 test2
,与我们上面的分析一致,表明优化器可能比不寻常的更能识别标准模式)。但是 GCC 不会这样做。同样,不能保证执行特定的优化。It seems that the only difference is if the top "mov"s are done before or after the "je".
Now (sorry my assembly is a bit crude), isn't it always better to have the movs after the jump, in order to save pipeline flushes?
不,不是真的。在这种情况下,这不会改进代码。如果分支被错误预测,无论如何你都会有一个管道刷新。推测性错误预测的代码是否是
ret
并不重要指令或者它是 mov
指令。唯一重要的原因是
ret
紧跟在条件分支之后的指令是如果您正在手工编写汇编代码并且不知道使用 rep ret
指令。这是 a trick necessary for certain AMD processors that avoids a branch-prediction penalty .除非您是 assembly 专家,否则您可能不会知道这个技巧。但是编译器可以,并且也知道当您专门针对没有这种怪癖的 Intel 处理器或不同代的 AMD 处理器时,没有必要这样做。但是,您可能认为拥有
mov
更好。 s 在分支之后,但不是因为您建议的原因。现代处理器(我相信这是 Nehalem 和更高版本,但如果我需要验证,我会在 Agner Fog's excellent optimization guides 中查找)在某些情况下能够进行宏操作融合。基本上,宏操作融合意味着 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
test2
的目标代码之间的另一个区别和 test3
是代码大小。感谢优化器发出的用于对齐分支目标的填充,test3
的代码比 test2
大 4 个字节.然而,这不太可能是足够重要的差异,特别是如果这段代码不是在一个紧密循环中执行的,它保证在缓存中很热。那么,这是否意味着您应该始终像在
test2
中所做的那样编写代码? ?嗯,不,有几个原因:
即使在这里,它看起来会产生非常简单且可能更优化的代码,但实际上可能会更慢,因为在
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
这样的函数时。在一个紧密的循环内。 条件移动非常适合避免分支错误预测,但它们具有增加依赖链长度的重要缺点。相比之下,正确预测的分支会破坏依赖链。 (这可能就是当您添加额外变量时 GCC 不发出两个 CMOV 指令的原因。)如果您预计分支预测失败,条件移动只是性能上的胜利。如果您可以指望约 75% 或更高的预测成功率,条件分支可能会更快,因为它打破了依赖链并具有更低的延迟。我怀疑这里就是这种情况,除非
c
每次调用该函数时,都会在 99 和非 99 之间快速来回交替。 (参见 Agner Fog's "Optimizing subroutines in assembly language",第 70-71 页。)关于c++ - if else 比 if + default 快吗?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/41359023/