我正在尝试做一些内联汇编,以在输入b的给定位置测试一点,如果它的a为1,它将用XOR b替换b。
我遇到错误“ bt的操作数大小不匹配”。
当我使用-O3进行编译时,有时看起来像预期的那样工作。它完全不一致。有时包括正确地进行计算,有时在编译时给出错误。 (全部带有-O3)。
没有-O3的情况下,总是在编译时给出错误。
对此的总体要求是尽可能快,并且可以在大多数现代AMD64处理器上运行。
unsigned long ConditionAdd(const unsigned long a, unsigned long b, const long pos) {
// Intended behavior: Looks at bit in position pos in b: if that bit is a 1, then it replaces b with a xor b
asm (
"BT %[position], %[changeable] \n\t"
"JNC to_here%=\n\t"
"XOR %[unchangeable], %[changeable]\n\t"
"to_here%=: "
: [changeable] "=&r" (b)
: [position] "mr" (pos), [unchangeable] "mr" (a), "[changeable]" (b)
: "cc"
);
return b;
}
答案 0 :(得分:1)
没有-O3的情况下,总是在编译时给出错误。
您为编译器提供了为bt
源操作数选择内存的选项。在禁用优化的情况下,它会这样做,因此结果不会合并。 bt $imm, r/m
或bt %reg, r/m
是唯一可编码的形式。 (使用手册bt r/m, imm
或bt r/m, reg
之类的英特尔语法)。
幸运的是,您没有给bt
选择为bt
选择存储目的地的选项,因为它具有疯狂的CISC位字符串语义,这使得它在reg,mem情况下非常慢,例如5 uops。在Ryzen上,在SnB系列上为10 oups。 (https://agner.org/optimize/和/或https://uops.info)。您始终希望编译器首先将操作数加载到寄存器中。
您最多只能读一次a
,因此将其存储在内存中是合理的。 OTOH,如果您选择"m"
或"rm"
,clang总是会选择"mr"
。这是一个已知的错过优化的错误,但是如果您关心clang,通常不是一个好主意,不要给编译器该选项,否则它将在asm语句之前溢出寄存器。
别忘了允许a
和pos
的立即数。 xor
采用32位符号扩展的立即数,因此您需要{{ 3}}。 64位移位计数可以使用"J"
约束来限制0..63中的立即数,也可以让bt
为您屏蔽(也称为模)源操作数,即使它是立即数也是如此。但这是GAS使用bt
中不适合的imm8
立即数的汇编时错误。因此,您可以通过仅使用pos
约束来使用它来检测太大的编译时常数i
。您还可以想象一下,当.ifeq
是数字时,可以使用%[pos] & 63
asm宏来完成%[pos]
,否则只需%[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
的{{1}}对性能计数器进行配置。
您可能仍希望使用内联汇编,因为gcc通常很难使用branch-misses
来优化bt
或a & (1ULL << (pos&63))
之类的{它的{1}} / btc/s/r
/ ^=
版本。 (有时,包括这里的声音会更好)。
您可能希望|=
/ &= ~
/ bt
在XOR运算之前有条件地将cmov
的tmp副本归零,因为xor
是加法器/ xor身份值:a
。为cmov创建归零的reg可能比在reg中创建0 / -1更好(例如,使用0
/ b ^ 0 == b
/ bt
/ sbb same,same
)。在Broadwell及更高版本以及AMD上,and %tmp, %a
仅1 uop。零归零可能成为延迟的关键路径。只有AMD具有打破依赖关系的xor %a, %b
,而在Intel上,如果cmov
为2 ups,则为2 uop。
将相关位移到寄存器的顶部以用sbb same,same
进行广播是另一种选择,但是我认为您需要在寄存器中使用cmov
。对于无分支机构,这仍然比cmov更糟糕。
但是最简单的方法是使用sar reg, 63
在63-pos
和cmov
之间进行选择,特别是如果可以销毁获得b
的寄存器的话。(即如果编译器想保留它,则强制编译器b^a
复制它)
但是您是否尝试过使用纯C来使编译器内联和优化?特别是如果恒定传播是可能的。 "e"
constraint还是可以通过SIMD自动向量化,其中a
,mov
和a
来自数组?
(当某些输入是使用b
的编译时常量时,您可以使用纯C。除了clang7.0及更早版本中,它是在内联之前求值的,因此对于包装器函数始终为false。)>
IDK(如果您有意选择了pos
),它在Windows ABI中是32位,在x86-64 System V上是64位。在32位模式下是32位。您的asm不区分宽度(16位,32位或64位。__builtin_constant_p
没有8位操作数大小的版本)。如果您的意思是绝对是64位,请使用unsigned long
。
bt
使用clang9.0 https://gcc.gnu.org/wiki/DontUseInlineAsm编译为
uint64_t
但是GCC基本上是按书面形式编译的。尽管它将为您优化// 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 -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
。 b & (1UL<<pos)
的BMI2(单变量变量计数移位而不是(b>>pos) & 1
在Intel上为3 uop)有帮助,所以我使用了包含它的shlx
。 (Haswell或更新的版本/ Ryzen或更新的版本)
shr %cl, %reg
shrx + andl等同于BT,除了它设置ZF而不是CF。
如果您无法启用BMI2且必须使用GCC,那么-march
的内联汇编可能是个好主意。
否则,请使用clang并获得接近最佳的无分支汇编。
我认为,通过预先计算# 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
并使用bt
在b^a
之间进行选择,将关键路径缩短1个周期,可以使clang做得更好,因为这可以与cmov
。
b
请注意ILP:前3条指令完全取决于输入。 (bt
不写其目标注册表)。 RSI准备就绪后,它们都可以在第一个周期中执行。然后,用于下一个周期的CMOV的所有3个输入(RDI,RAX和CF)都准备就绪。因此,假设单脉冲CMOV,总延迟=来自每个输入/每个输入2个周期。
您可以使用tmp reg轻松将其转换为inline-asm,并将硬编码的regs转换回# 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
操作数。确保告诉编译器bt
输入已被读/写破坏。您可以使用单独的%[name]
输入和a
输出,让编译器根据需要选择不同的寄存器。您不需要匹配约束。
"r"(b)
仅适用于"=r"(b)
考虑仅使用内联汇编来用标志输出操作数asm
包装bt
,并将其余的留给编译器。这可以让您从gcc获得更好的asm,而不必将整个事情放入asm。这允许对bt
和"=@ccc"(cf_output)
进行constprop和其他可能的优化,并在布局函数时提供GCC灵活性。例如它不必一口气完成所有工作,它可以与其他工作交叉进行。 (对于OoO高管来说没什么大不了的。)