【问题标题】:gcc inline assembly - operand type mismatch for `add', trying to create branchless codegcc 内联汇编 - 'add' 的操作数类型不匹配,试图创建无分支代码
【发布时间】:2012-12-10 23:24:31
【问题描述】:

我正在尝试做一些代码优化来消除分支,原来的c代码是

if( a < b ) 
   k = (k<<1) + 1;
else
   k = (k<<1)

我打算用下面的汇编代码替换它

mov a, %rax 
mov b, %rbx
mov k, %rcx
xor %rdx %rdx
shl 1, %rcx
cmp %rax, %rax
setb %rdx
add %rdx,%rcx
mov %rcx, k 

所以我写了像blow这样的c内联汇编代码,

#define next(a, b, k)\
 __asm__("shl $0x1, %0; \
         xor %%rbx, %%rbx; \
         cmp %1, %2; \
         setb %%rbx; \
         addl  %%rbx,%0;":"+c"(k) :"g"(a),"g"(b))

当我编译下面的代码时出现错误:

operand type mismatch for `add'
operand type mismatch for `setb'

我该如何解决?

【问题讨论】:

  • 除非你的编译器真的很糟糕,否则你应该能够在不求助于 asm 的情况下消除分支,例如k = (k &lt;&lt; 1) + (a &lt; b); 应该生成无分支代码。
  • 尽管为这段代码编写 asm 从根本上说是错误的,但这里仍然存在一个有效的问题:如何修复 asm 以使其编译并执行预期的操作。
  • @R.. 这很容易回答。编译 C 代码并研究编译器的输出。
  • @DavidHeffernan:实际上这不会有帮助。 OP 的问题似乎是无效的约束或操作数。由于 inline asm 与独立 asm 有很大不同,因此仅查看生成的 asm 并不能解决 inline asm 问题。

标签: c performance assembly x86 inline-assembly


【解决方案1】:

以下是代码中的错误:

  1. 错误:'cmp' 的操作数类型不匹配 -- CMP 的操作数之一必须是寄存器。您可能正在生成试图比较两个立即数的代码。将第二个操作数的约束从 "g" 更改为 "r"。 (见GCC Manual - Extended Asm - Simple Constraints
  2. 错误:'setb' 的操作数类型不匹配 -- SETB 只需要 8 位操作数,即 setb %bl 有效,而 setb %rbx 无效。
  3. C 表达式 T = (A &lt; B) 应转换为 AT&T x86 汇编语法中的 cmp B,A; setb TCMP 的两个操作数顺序错误。请记住,CMP 的工作方式类似于 SUB

一旦您意识到前两个错误消息是由汇编程序产生的,那么调试它们的技巧就是查看 gcc 生成的汇编程序代码。尝试gcc $CFLAGS -S t.c 并将t.s 中有问题的行与x86 opcode reference 进行比较。关注每条指令允许的operand codes,您会很快发现问题。

在下面发布的固定源代码中,我假设您的操作数是无符号的,因为您使用的是 SETB 而不是 SETL。我从使用 RBX 切换到 RCX 来保存临时值,因为 RCX 是 ABI 中的调用破坏寄存器并使用了 "=&amp;c" 约束将其标记为 earlyclobber 操作数,因为在读取输入 ab 之前清除了 RCX

#include <stdio.h>
#include <stdint.h>
#include <inttypes.h>

static uint64_t next(uint64_t a, uint64_t b, uint64_t k)
{
    uint64_t tmp;
    __asm__("shl $0x1, %[k];"
        "xor %%rcx, %%rcx;"
        "cmp %[b], %[a];"
        "setb %%cl;"
        "addq %%rcx, %[k];"
        : /* outputs */ [k] "+g" (k), [tmp] "=&c" (tmp)
        : /* inputs  */ [a] "r" (a), [b] "g" (b)
        : /* clobbers */ "cc");
    return k;
}

int main()
{
    uint64_t t, t0, k;
    k = next(1, 2, 0);
    printf("%" PRId64 "\n", k);

    scanf("%" SCNd64 "%" SCNd64, &t, &t0);
    k = next(t, t0, k);
    printf("%" PRId64 "\n", k);

    return 0;
}

ma​​in() 转换为:

<+0>:   push   %rbx
<+1>:   xor    %ebx,%ebx
<+3>:   mov    $0x4006c0,%edi
<+8>:   mov    $0x1,%bl
<+10>:  xor    %eax,%eax
<+12>:  sub    $0x10,%rsp
<+16>:  shl    %rax
<+19>:  xor    %rcx,%rcx
<+22>:  cmp    $0x2,%rbx
<+26>:  setb   %cl
<+29>:  add    %rcx,%rax
<+32>:  mov    %rax,%rbx
<+35>:  mov    %rax,%rsi
<+38>:  xor    %eax,%eax
<+40>:  callq  0x400470 <printf@plt>
<+45>:  lea    0x8(%rsp),%rdx
<+50>:  mov    %rsp,%rsi
<+53>:  mov    $0x4006c5,%edi
<+58>:  xor    %eax,%eax
<+60>:  callq  0x4004a0 <__isoc99_scanf@plt>
<+65>:  mov    (%rsp),%rax
<+69>:  mov    %rbx,%rsi
<+72>:  mov    $0x4006c0,%edi
<+77>:  shl    %rsi
<+80>:  xor    %rcx,%rcx
<+83>:  cmp    0x8(%rsp),%rax
<+88>:  setb   %cl
<+91>:  add    %rcx,%rsi
<+94>:  xor    %eax,%eax
<+96>:  callq  0x400470 <printf@plt>
<+101>: add    $0x10,%rsp
<+105>: xor    %eax,%eax
<+107>: pop    %rbx
<+108>: retq   

您可以在每次调用printf() 之前看到将next() 移入RSI 的结果。

【讨论】:

  • 你真的应该使用"+r" 代替k,因为你想强制编译器加载到寄存器中,而不是使用内存目标移位和内存目标添加。
  • 顺便说一句,“g”约束对于b 是不安全的。您的版本与不适合 32 位符号扩展立即数的 55555555555555 之类的大立即数中断。您需要"rme" 才能允许 reg、mem 或 32 位立即数。如果b 在寄存器或立即数中,请参阅我的答案以获取允许a 在内存中的多选项约束。很好地回答了为什么 OP 的代码不能编译,但是关于 OP 的内联如何好多更好:P
【解决方案2】:

鉴于 gcc(它看起来像 gcc 内联汇编器)产生:

leal    (%rdx,%rdx), %eax
xorl    %edx, %edx
cmpl    %esi, %edi
setl    %dl
addl    %edx, %eax
ret

来自

int f(int a, int b, int k)
{
  if( a < b ) 
    k = (k<<1) + 1;
  else
    k = (k<<1);

  return k;
}

它会认为编写自己的内联汇编程序完全是浪费时间和精力。

与往常一样,在开始编写内联汇编程序之前,请检查编译器实际执行的操作。如果您的编译器不生成此代码,那么您可能需要将编译器的版本升级到更新的版本(我向 Jan Hubicka [当时 x86-64 的 gcc 维护者] ca 2001 报告了这种事情,并且我敢肯定它在 gcc 中已经存在了一段时间)。

【讨论】:

  • gcc 这些天变得更聪明了:你得到leal / cmpl / adcl $0, %eax。 (或者它可能已经那么聪明了:当 OP 使用无符号时,您使用了有符号比较,因此 CF 不是比较结果。)无论如何,请参阅我对现代编译器的编译器输出的回答。
【解决方案3】:

你可以这样做,编译器不会生成分支:

k = (k<<1) + (a < b) ;

但是如果你必须的话,我在你的代码中修复了一些东西,现在它应该可以按预期工作了:

__asm__(
        "shl  $0x1, %0; \
        xor  %%eax, %%eax; \
        cmpl %3, %2; \
        setb %%al; \
        addl %%eax, %0;"
        :"=r"(k)        /* output */
        :"0"(k), "r"(a),"r"(b)  /* input */
        :"eax", "cc"   /* clobbered register */ 
);

请注意,setb 需要 reg8mem8 并且您应该将 eax 添加到已破坏列表中,因为您更改了它以及 cc 只是为了安全起见,至于寄存器约束,我不确定你为什么使用这些,但 =rr 工作得很好。 您需要将k 添加到输入和输出列表中。 GCC-Inline-Assembly-HOWTO 中还有更多内容

【讨论】:

  • 确实——任何体面的编译器都应该为此生成无分支代码。
  • @DavidHeffernan 我不确定,为什么会更好?
【解决方案4】:

总结:

  • 无分支可能甚至不是最佳选择。
  • Inline asm defeats some other optimizations, try other source changes first,例如? : 经常无分支编译,也使用布尔值作为整数 0/1。
  • 如果您使用 inline-asm,请确保同时优化约束以使编译器生成的代码在您的 asm 块之外高效。
  • 使用cmp %[b], %[a] / adc %[k],%[k] 可以做到这一点。 你的手写代码比编译器生成的要差,但在常量传播/ CSE /内联并没有使这段代码(部分)优化掉。

如果您的编译器生成分支代码,并且分析表明这是错误的选择(该指令中分支未命中的高计数,例如在 Linux perf record -ebranch-misses ./my_program && perf report 上),那么是的应该做一些事情来获得无分支代码。

(如果它是可预测的,分支可能是一个优势:分支意味着使用(k&lt;&lt;1) + 1 的代码的乱序执行不必等待ab 准备好。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&lt;&lt;1) + (a&lt;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&lt;b 是一个无符号比较(您正在使用setb,下面的无符号条件)。所以你的比较结果在进位标志中。 x86 有一个 add-with-carry 指令。此外,k&lt;&lt;1k+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)

编译器足够聪明,可以使用addlea 左移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&lt;&lt;1) + ( a &lt; 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/mcmp r/m, rcmp 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

没有setcadc $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&lt;&lt;1 作为输入。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2019-02-17
    • 1970-01-01
    • 1970-01-01
    • 2018-02-11
    • 2016-05-18
    • 1970-01-01
    • 1970-01-01
    • 2011-09-07
    相关资源
    最近更新 更多