总结:
如果您的编译器生成分支代码,并且分析表明这是错误的选择(该指令中分支未命中的高计数,例如在 Linux perf record -ebranch-misses ./my_program && perf report 上),那么是的应该做一些事情来获得无分支代码。
(如果它是可预测的,分支可能是一个优势:分支意味着使用(k<<1) + 1 的代码的乱序执行不必等待a 和b 准备好。LLVM 最近合并@ 987654322@,因为现代 x86 CPU 具有如此强大的分支预测器。Clang/LLVM nightly build(带有那个补丁)仍然为此 C 源代码选择无分支,至少在循环外的独立函数中)。
如果这是用于二分搜索,那么无分支可能是一个不错的策略,除非您经常看到相同的搜索。 (分支+推测执行意味着你有一个关键路径之外的控制依赖,
使用配置文件引导的优化进行编译,因此编译器具有运行时信息,说明哪些分支几乎总是单向运行。它可能仍然不知道一个难以预测的分支和一个整体上采用两条路径但具有简单模式的分支之间的区别。 (或者根据全局历史可以预测;很多modern branch-predictor designs index based on branch history,所以最后几个分支的走向决定了当前分支使用哪个表条目。)
相关:gcc optimization flag -O3 makes code slower then -O2 显示了一个情况,其中排序数组对循环内的条件进行近乎完美的分支预测,以及 gcc -O3 的无分支代码(没有配置文件引导优化)对使用数据依赖性的瓶颈cmov。但是-O3 -fprofile-use 会生成分支代码。 (另外,一种不同的编写方式可以使低延迟的无分支代码也更好地自动矢量化。)
如果你不能hand-hold the compiler into making the asm you want,内联汇编应该是你最后的选择,例如按照其他人的建议将其写为(k<<1) + (a<b)。
内联汇编破坏了许多优化,最明显的常量传播(如在其他一些答案中所见,其中 gcc 将常量移动到内联汇编代码块之外的寄存器中)。 https://gcc.gnu.org/wiki/DontUseInlineAsm.
当编译器为部分/所有变量提供常量值时,您可以使用 if(__builtin_constant_p(a)) 等来使用纯 C 版本,但这需要更多工作。 (并且不适用于 Clang,其中 __builtin_constant_p() 在函数内联之前被评估。)
即使那样(一旦您将事情限制在输入不是编译时常量的情况下),也不可能为编译器提供全部选项,因为您不能使用不同的 asm 块,具体取决于哪些约束是匹配的(例如,a 在寄存器中,b 在内存中,反之亦然。)如果你想根据情况使用不同的指令,你就搞砸了,但在这里我们可以使用 multi - 替代约束以暴露cmp 的大部分灵活性。
让编译器生成接近最优的代码通常比使用内联 asm 更好。 Inline-asm 破坏了编译器重用任何临时结果的能力,或者分散指令与其他编译器生成的代码混合的能力。 (由于良好的乱序执行,指令调度在 x86 上并不是什么大问题,但仍然如此。)
那个 asm 很垃圾。如果你有很多分支未命中,它比分支实现要好,但是很多更好的无分支实现是可能的。
您的a<b 是一个无符号比较(您正在使用setb,下面的无符号条件)。所以你的比较结果在进位标志中。 x86 有一个 add-with-carry 指令。此外,k<<1 与k+k 是一回事。
所以你想要的 asm(编译器生成或内联 asm)是:
# k in %rax, a in %rdi, b in %rsi for this example
cmp %rsi, %rdi # CF = (a < b) = the carry-out from edi - esi
adc %rax, %rax # eax = (k<<1) + CF = (k<<1) + (a < b)
编译器足够聪明,可以使用add 或lea 左移1,有些编译器足够聪明,可以使用adc 而不是setb,但他们无法将两者结合起来。
使用寄存器 args 和返回值编写函数通常是了解编译器可能会做什么的好方法,尽管它确实会强制它们在不同的寄存器中产生结果。 (另请参阅 this Q&A 和 Matt Godbolt 的 CppCon2017 演讲:“What Has My Compiler Done for Me Lately? Unbolting the Compiler's Lid”)。
// I also tried a version where k is a function return value,
// or where k is a global, so it's in the same register.
unsigned funcarg(unsigned a, unsigned b, unsigned k) {
if( a < b )
k = (k<<1) + 1;
else
k = (k<<1);
return k;
}
On the Godbolt compiler explorer,以及其他几个版本。 (我在这个版本中使用了 unsigned,因为你的 asm 中有 addl。使用 unsigned long 可以将除异或归零之外的所有内容都变成 64 位寄存器。(xor %eax,%eax 仍然是零 RAX 的最佳方法。 )
# gcc7.2 -O3 When it can keep the value in the same reg, uses add instead of lea
leal (%rdx,%rdx), %eax #, <retval>
cmpl %esi, %edi # b, a
adcl $0, %eax #, <retval>
ret
#clang 6.0 快照 -O3
xorl %eax, %eax
cmpl %esi, %edi
设置%al
leal (%rax,%rdx,2), %eax
回复
# ICC18,和 gcc 一样,但是不能保存一个 MOV
添加 %edx, %edx #14.16
cmpl %esi, %edi #17.12
adcl $0, %edx #17.12
movl %edx, %eax #17.12
保留 #17.12
MSVC 是唯一一个不用手动就不会生成无分支代码的编译器。 ((k<<1) + ( a < b ); 给了我们完全相同的 xor/cmp/setb / lea 序列和上面的 clang 一样(但使用 Windows x86-64 调用约定)。
funcarg PROC ; x86-64 MSVC CL19 -Ox
lea eax, DWORD PTR [r8*2+1]
cmp ecx, edx
jb SHORT $LN3@funcarg
lea eax, DWORD PTR [r8+r8] ; conditionally jumped over
$LN3@funcarg:
ret 0
内联汇编
其他答案很好地涵盖了您的实施问题。要调试内联 asm 中的汇编器错误,use gcc -O3 -S -fverbose-asm 以查看编译器向汇编器提供的内容,并填写了 asm 模板。您会看到addl %rax, %ecx 或其他内容。
此优化实现使用multi-alternative constraints 让编译器选择cmp $imm, r/m、cmp r/m, r 或cmp r, r/m 形式的CMP。我使用了两个替代方法,它们不是通过操作码而是通过哪一侧包含可能的内存操作数来分割事物。 "rme" 类似于 "g" (rmi),但仅限于 32 位符号扩展立即数)。
unsigned long inlineasm(unsigned long a, unsigned long b, unsigned long k)
{
__asm__("cmpq %[b], %[a] \n\t"
"adc %[k],%[k]"
: /* outputs */ [k] "+r,r" (k)
: /* inputs */ [a] "r,rm" (a), [b] "rme,re" (b)
: /* clobbers */ "cc"); // "cc" clobber is implicit for x86, but it doesn't hurt
return k;
}
I put this on Godbolt with callers that inline it in different contexts。 gcc7.2 -O3 实现了我们对独立版本的期望(带有寄存器 args)。
inlineasm:
movq %rdx, %rax # k, k
cmpq %rsi, %rdi # b, a
adc %rax,%rax # k
ret
我们可以通过内联到其他调用者来查看我们的约束的工作情况:
unsigned long call_with_mem(unsigned long *aptr) {
return inlineasm(*aptr, 5, 4);
}
# gcc
movl $4, %eax #, k
cmpq $55555, (%rdi) #, *aptr_3(D)
adc %rax,%rax # k
ret
使用更大的立即数,我们将movabs 放入寄存器。 (但在 "i" 或 "g" 约束下,gcc 会发出不汇编的代码,或截断常量,试图为 cmpq 使用较大的立即常量。)
比较我们从纯 C 中得到的:
unsigned long call_with_mem_nonasm(unsigned long *aptr) {
return handhold(*aptr, 5, 4);
}
# gcc -O3
xorl %eax, %eax # tmp93
cmpq $4, (%rdi) #, *aptr_3(D)
setbe %al #, tmp93
addq $8, %rax #, k
ret
没有setc 的adc $8, %rax 可能会更好,但是如果没有__builtin_constant_p() 上的k,我们无法从内联asm 中得到它。
clang 经常选择 mem 替代品(如果有的话),所以它这样做:/facepalm。不要使用内联汇编。
inlineasm: # clang 5.0
movq %rsi, -8(%rsp)
cmpq -8(%rsp), %rdi
adcq %rdx, %rdx
movq %rdx, %rax
retq
顺便说一句,除非您要优化转换为比较加法,否则您可以并且应该向编译器询问 k<<1 作为输入。