组装中的位测试后的条件XOR

时间:2019-10-08 00:28:04

标签: gcc assembly x86-64 inline-assembly micro-optimization

我正在尝试做一些内联汇编,以在输入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;
}

1 个答案:

答案 0 :(得分:1)

  

没有-O3的情况下,总是在编译时给出错误。

您为编译器提供了为bt源操作数选择内存的选项。在禁用优化的情况下,它会这样做,因此结果不会合并。 bt $imm, r/mbt %reg, r/m是唯一可编码的形式。 (使用手册bt r/m, immbt 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语句之前溢出寄存器。

别忘了允许apos的立即数。 xor采用32位符号扩展的立即数,因此您需要{{ 3}}。 64位移位计数可以使用"J"约束来限制0..63中的立即数,也可以让bt为您屏蔽(也称为模)源操作数,即使它是立即数也是如此。但这是GAS使用bt中不适合的imm8立即数的汇编时错误。因此,您可以通过仅使用pos约束来使用它来检测太大的编译时常数i。您还可以想象一下,当.ifeq是数字时,可以使用%[pos] & 63 asm宏来完成%[pos],否则只需%[pos]

顺便说一句,您最好也使用更简单的[changeable] "+&r"(b)读/写约束,而不是输出+匹配约束。这是告诉编译器完全相同的事情的一种更紧凑的方式。

而且,您不需要早点聊天bt不会修改任何整数寄存器,只会修改EFLAGS,因此在最终读取纯输入操作数之前,不会在之前写入任何寄存器。如果已知ab拥有相同的值,那么将生成的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处理器上运行。

然后,您可能根本不需要条件分支。分支代码取决于分支的预测能力。现代分支预测器非常有用,但是如果分支与先前的分支或其他某种模式不相关,那么您就被搞砸了。使用针对perfbranches的{​​{1}}对性能计数器进行配置。

可能仍希望使用内联汇编,因为gcc通常很难使用branch-misses来优化bta & (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, 6363-poscmov之间进行选择,特别是如果可以销毁获得b的寄存器的话。(即如果编译器想保留它,则强制编译器b^a复制它)


但是您是否尝试过使用纯C来使编译器内联和优化?特别是如果恒定传播是可能的。 "e" constraint还是可以通过SIMD自动向量化,其中amova来自数组?

(当某些输入是使用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 并使用btb^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高管来说没什么大不了的。)