没有 -O3 它总是在编译时出错。
您为编译器提供了为bt 源操作数选择内存的选项。在禁用优化的情况下,它确实如此,因此结果不会组合。 bt $imm, r/m 或 bt %reg, r/m 是唯一可编码的形式。 (如手册中的英特尔语法,bt r/m, imm 或 bt r/m, reg)。
幸运的是,您没有给 bt 选择为 bt 选择内存目标的选项,因为它具有疯狂的 CISC 位串语义,这使得它在 reg、mem 情况下非常慢,就像 Ryzen 上的 5 uops、10对SnB家族的微博。 (https://agner.org/optimize/ 和/或https://uops.info)。您总是希望编译器首先将操作数加载到寄存器中。
您最多只能读取一次a,因此将其保存在内存中是合理的。 OTOH,如果您选择"rm" 或"mr",clang 总是选择"m"。这是一个已知的错过优化错误,但如果您关心 clang,通常最好不为编译器提供该选项,否则它将在 asm 语句之前溢出寄存器。
不要忘记允许 a 和 pos 的立即数。 xor 采用 32 位符号扩展立即数,因此您需要 "e" constraint。 64 位移位计数可以使用 "J" 约束来限制 0..63 中的立即数,或者您可以让 bt 为您屏蔽(又名模)源操作数,即使它是立即数。但是 GAS 使用不适合 imm8 的 bt 立即数是一个汇编时错误。因此,您可以使用它来检测编译时间常数pos,仅使用i 约束就太大了。你也可以想象当%[pos] 是数字时做.ifeq asm 宏的东西来做%[pos] & 63,否则只是%[pos]。
顺便说一句,您不妨使用更简单的[changeable] "+&r"(b) 读/写约束,而不是输出+匹配约束。这是告诉编译器完全相同的事情的更简洁的方式。
而且,你不需要早起的破坏者。 bt 不修改任何整数寄存器,只修改 EFLAGS,因此在对仅输入操作数的最终读取之前不会写入任何寄存器。如果已知a 和b 具有相同的值,则生成的asm 将xor same,same(一个归零习惯用法)作为贯穿路径是完全可以的。
unsigned long ConditionAdd(const unsigned long a, unsigned long b, const long pos) {
// if (b & (1UL<<pos)) b ^= a;
asm (
"BT %[position], %[changeable] \n\t"
"JNC to_here%=\n\t"
"XOR %[unchangeable], %[changeable]\n\t"
"to_here%=: "
: [changeable] "+r" (b)
: [position] "ir" (pos), [unchangeable] "er" (a)
: "cc" // cc clobber is already implicit on x86, but doesn't hurt
);
return b;
}
对此的总体要求是尽可能快,并且可以在大多数现代 AMD-64 处理器上运行。
那么您可能根本不需要条件分支。分支代码依赖于分支预测良好。现代分支预测器非常漂亮,但如果分支与前面的分支或其他模式不相关,那么你就完蛋了。使用perf 为branches 和branch-misses 配置性能计数器。
您可能仍然需要内联 asm,因为 gcc 通常很难使用 bt 来优化 a & (1ULL << (pos&63)) 或 btc/s/r 之类的东西 ^= / |= /&= ~ 的版本。 (有时 clang 会更好,包括这里)。
您可能希望 bt / cmov / xor 在 XORing 之前有条件地将 a 的 tmp 副本归零,因为 0 是加法 / xor 标识值:b ^ 0 == b。为 cmov 创建一个归零的 reg 可能比在 reg 中创建一个 0 / -1 更好(例如,使用 bt / sbb same,same / and %tmp, %a / xor %a, %b)。在 Broadwell 及更高版本以及 AMD 上,cmov 仅为 1 uop。异或归零可能是延迟的关键路径。只有 AMD 具有打破依赖关系的 sbb same,same,而在 Intel 上,如果 cmov 为 2 微指令,则为 2 微指令。
将相关位移到寄存器顶部以使用sar reg, 63 广播它是另一种选择,但我认为您需要在寄存器中使用63-pos。对于无分支,这仍然会比 cmov 更糟糕。
但简单地使用cmov 在b 和b^a 之间进行选择是最有意义的,特别是如果可以销毁您获得a 的寄存器。(即强制编译器复制到mov如果它想保留它)
但是您是否尝试过使用纯 C 让编译器内联和优化?特别是如果可以进行持续传播。 https://gcc.gnu.org/wiki/DontUseInlineAsm 或者是否可以使用 SIMD 自动矢量化,a、b 和 pos 来自数组?
(当某些输入是使用__builtin_constant_p 的编译时常量时,您可以回退到纯C。除了在clang7.0 和更早版本中,它在内联之前进行评估,因此对于包装函数它总是错误的。)
IDK 如果您有意选择unsigned long,它在 Windows ABI 中为 32 位,在 x86-64 System V 上为 64 位。在 32 位模式下为 32 位。您的 asm 与宽度无关(16、32 或 64 位。bt 没有 8 位操作数大小的版本)。如果您的意思是绝对是 64 位,那么请使用 uint64_t。
// you had an editing mistake in your function name: it's Xor not Add.
unsigned long ConditionXor(const unsigned long a, unsigned long b, const long pos) {
if ((b>>pos) & 1) {
b ^= a;
}
return b;
}
用clang9.0 on the Godbolt compiler explorer编译成
# clang9.0 -O3 -Wall -march=skylake
ConditionXor(unsigned long, unsigned long, long)
xorl %eax, %eax # rax=0
btq %rdx, %rsi
cmovbq %rdi, %rax # rax = CF ? a : 0
xorq %rsi, %rax # rax = b ^ (a or 0)
retq
但 GCC 基本上按原样编译。虽然它会为您优化b & (1UL<<pos) 到(b>>pos) & 1。 shlx 的 BMI2(单 uop 变量计数移位而不是 Intel 上的 3 uop 用于 shr %cl, %reg)有帮助,所以我使用了包含它的 -march。 (Haswell 或更新/Ryzen 或更新)
# gcc9.2 -O3 -march=haswell
ConditionXor(unsigned long, unsigned long, long):
xorq %rsi, %rdi # a^b
movq %rsi, %rax # retval = b
shrx %rdx, %rsi, %rdx # tmp = b>>pos
andl $1, %edx # test $1, %dl might be better
cmovne %rdi, %rax # retval = (b>>pos)&1 ? a^b : b
ret
shrx+andl 等价于 BT,只不过它设置的是 ZF 而不是 CF。
如果您无法启用 BMI2 并且必须使用 GCC,那么 bt 的内联 asm 可能是个好主意。
否则使用 clang 并获得接近最佳的无分支汇编。
我认为 clang 可以通过预先计算 b^a 并使用 cmov 在 b 之间进行选择来做得更好,从而将关键路径缩短 1 个周期,因为这可以与 bt 并行发生。
# hand-written probably-optimal branchless sequence. Like GCC but using BT
xor %rsi, %rdi # destroy tmp copy of a, in a "+&r"(tmp)
bt %rdx, %rsi
mov %rsi, %rax # mov to an "=r"(output)
cmovc %rdi, %rax
ret
注意 ILP:前 3 条指令都完全依赖于输入。 (bt 不写它的目标寄存器)。它们都可以在 RSI 就绪后的第一个周期内执行。然后 CMOV 的所有 3 个输入都准备好下一个周期(RDI、RAX 和 CF)。所以总延迟 = 来自任何/每个输入的 2 个周期,假设是单微指令 CMOV。
您可以使用 tmp reg 轻松地将其转换回 inline-asm,并将硬编码的 reg 转换回 %[name] 操作数。确保告诉编译器 a 输入已通过使其读/写被破坏。您可以使用单独的"r"(b) 输入和"=r"(b) 输出,让编译器根据需要选择不同的寄存器。您不需要匹配的约束。
中间地带:asm 仅适用于bt
考虑使用内联 asm 仅将 bt 与标志输出操作数 "=@ccc"(cf_output) 包装在一起,并将其余部分留给编译器。这可以让您从 gcc 中获得更好的 asm,而无需将整个内容放入 asm.xml 中。这允许对 a 和 a^b 进行 constprop 和其他可能的优化,并在布局函数时为 GCC 提供灵活性。例如它不必在一个小块中完成所有工作,它可以与其他工作交错。 (考虑到 OoO exec,这没什么大不了的)。