【问题标题】:Conditional XOR after Bit Test in Assembly组装中位测试后的条件异或
【发布时间】:2019-10-08 00:28:04
【问题描述】:

我正在尝试做一些内联汇编,在输入 b 的给定位置进行测试,如果它是 1,它将 b 替换为 XOR 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;
}

【问题讨论】:

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


    【解决方案1】:

    没有 -O3 它总是在编译时出错。

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

    不要忘记允许 apos 的立即数。 xor 采用 32 位符号扩展立即数,因此您需要 "e" constraint。 64 位移位计数可以使用 "J" 约束来限制 0..63 中的立即数,或者您可以让 bt 为您屏蔽(又名模)源操作数,即使它是立即数。但是 GAS 使用不适合 imm8bt 立即数是一个汇编时错误。因此,您可以使用它来检测编译时间常数pos,仅使用i 约束就太大了。你也可以想象当%[pos] 是数字时做.ifeq asm 宏的东西来做%[pos] & 63,否则只是%[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 处理器上运行。

    那么您可能根本不需要条件分支。分支代码依赖于分支预测良好。现代分支预测器非常漂亮,但如果分支与前面的分支或其他模式不相关,那么你就完蛋了。使用perfbranchesbranch-misses 配置性能计数器。

    可能仍然需要内联 asm,因为 gcc 通常很难使用 bt 来优化 a &amp; (1ULL &lt;&lt; (pos&amp;63))btc/s/r 之类的东西 ^= / |= /&amp;= ~ 的版本。 (有时 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 更糟糕。

    但简单地使用cmovbb^a 之间进行选择是最有意义的,特别是如果可以销毁您获得a 的寄存器。(即强制编译器复制到mov如果它想保留它)


    但是您是否尝试过使用纯 C 让编译器内联和优化?特别是如果可以进行持续传播。 https://gcc.gnu.org/wiki/DontUseInlineAsm 或者是否可以使用 SIMD 自动矢量化,abpos 来自数组?

    (当某些输入是使用__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 &amp; (1UL&lt;&lt;pos)(b&gt;&gt;pos) &amp; 1shlx 的 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 并使用 cmovb 之间进行选择来做得更好,从而将关键路径缩短 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 中。这允许对 aa^b 进行 constprop 和其他可能的优化,并在布局函数时为 GCC 提供灵活性。例如它不必在一个小块中完成所有工作,它可以与其他工作交错。 (考虑到 OoO exec,这没什么大不了的)。

    【讨论】:

    • 感谢您的回复!我仍在阅读所有这些内容,因为它需要筛选很多(尤其是对于像我这样的新手!)。我只想指出,将其命名为 ConditionAdd 是故意的,因为字段 F_2 中的 XOR 等同于加法。也许会产生误导,但作为数学家,它非常相关!
    • 没有使用 asm flags 的变体?您可以使用 BT 获取位,然后使用 C 进行异或。铿锵声似乎like这个。
    • @DavidWohlferd:哦,这是个好主意,我还没有意识到 clang 终于添加了 GCC6 标志输出支持。但是由于函数调用或其他原因,GCC 在bt 的结果上使用了setc / test 太糟糕了! godbolt.org/z/zJ14kV。它还部分地破坏了持续传播,当然也使得自动矢量化成为不可能。 (AVX2 可以使用 vpsllvq 来实现 b&lt;&lt;(63-pos) 并使用该高位作为混合控制。或其他可能性。IDK 如果 GCC/clang 实际上会为每个元素变量的移位计数做到这一点。)跨度>
    • 看起来标志可能比您的最佳标志更好(微观)一点。至于持续传播,如果这是一个真正的可能性,你总是可以在 asm 调用周围包装一个 __builtin_constant_p 并使用你的 C 代码来使其他情况受益。 this 之类的东西(在 Godbolt 之前我们是如何运作的)?由于__builtin_constant_p 是一个编译时函数,它不会影响(优化)执行。自动矢量化超出了我的范围。
    • 我不会经常找到你错过的东西,所以今天是个好日子。也就是说,gcc9.2(我假设 OP 正在使用的)在这种情况下使用标志几乎没有优化。这使您的答案比我的更好(这就是我不发布答案的原因)。
    猜你喜欢
    • 2019-09-17
    • 2022-01-02
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-07-19
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多