首页 文章

在内联汇编的多个替代操作数约束之间进行选择时,GCC可以发出不同的指令助记符吗?

提问于
浏览
5

我正在尝试为GCC编写内联x86-64程序集以有效地使用MULQ指令 . MULQ将64位寄存器RAX与另一个64位值相乘 . 另一个值可以是任何64位寄存器(甚至是RAX)或内存中的值 . MULQ将产品的高64位放入RDX,将低64位放入RAX .

现在,很容易表达一个正确的mulq作为内联汇编:

#include <stdint.h>
static inline void mulq(uint64_t *high, uint64_t *low, uint64_t x, uint64_t y)
{
    asm ("mulq %[y]" 
          : "=d" (*high), "=a" (*low)
          : "a" (x), [y] "rm" (y)    
        );
}

此代码是正确的,但不是最佳的 . MULQ是可交换的,所以如果 y 恰好在RAX中,那么将 y 保留在原来的位置并进行乘法是正确的 . 但GCC不知道这一点,因此会发出额外的指令将操作数移动到预先定义的位置 . 我想告诉GCC它可以将任一输入放在任一位置,只要一个在RAX中结束而MULQ引用另一个位置 . GCC有一个语法,称为"multiple alternative constraints" . 注意逗号(但是整个asm()被破坏了;见下文):

asm ("mulq %[y]" 
      : "=d,d" (*high), "=a,a" (*low)
      : "a,rm" (x), [y] "rm,a" (y)    
    );

不幸的是,这是错误的 . 如果GCC选择第二个替代约束,它将发出“mulq%rax” . 要清楚,请考虑以下功能:

uint64_t f()
{
    uint64_t high, low;
    uint64_t rax;
    asm("or %0,%0": "=a" (rax));
    mulq(&high, &low, 7, rax);
    return high;
}

gcc -O3 -c -fkeep-inline-functions mulq.c 编译,GCC发出这个程序集:

0000000000000010 <f>:
  10:   or     %rax,%rax
  13:   mov    $0x7,%edx
  18:   mul    %rax
  1b:   mov    %rdx,%rax
  1e:   retq

“mul%rax”应为“mul%rdx” .

如何重写这个内联asm,以便在每种情况下生成正确的输出?

3 回答

  • 2
    __asm__ ("mulq %3" : "=a,a" (*low), "=d,d" (*high) : "%0,0" (x), "r,m" (y))
    

    这类似于各种GNU包中包含的 longlong.h 中的内容; "r,m" 而不是 "rm" 真的是为了clang的利益 . 对于clang,多重约束语法似乎仍然很重要,如here所述 . 这是一种耻辱,但我仍然发现clang在约束匹配(尤其是在x86 [-86]上)比gcc更糟糕 . 对于gcc:

    __asm__ ("mulq %3" : "=a" (*low), "=d" (*high) : "%0" (x), "rm" (y))
    

    这将是足够的,并且有利于将 (y) 保留在登记簿中,除非登记压力太高;但在许多情况下,铿锵似乎总是泄漏 . 我的测试显示它将在多重约束语法中选择第一个选项 "r" .

    "%3" 作为指令中的被乘数允许寄存器(偏好)或存储器位置,作为第三个操作数的别名,相对于零,即 (y) . "0" 别名为'zero-th'操作数: (*low) ,明确为 "a" ,即 %rax 为64位 . "%0" 中的前导 % 字符是可交换运算符:即,(x)可以与(y)通信,如果这有助于寄存器分配 . 显然, mulq 是可交换的: x * y == y * x .

    我们实际上在这里受到很大限制 . mulq 将64位操作数 %3 乘以 %rax 中的值以生成128位乘积: %rdx:%rax . "0" (x) 意味着必须将 (x) 加载到 %rax 中,并且必须将 (y) 加载到64位寄存器或内存地址中 . 但是 %0 表示 (x) ,以下输入 (y) 可能会通勤 .

    我也会参考我发现的best practical inline assembly tutorial . 虽然gcc引用是'authoritative',但它们是一个糟糕的教程 .


    感谢Chris在原始约束排序中拾取错误 .

  • 0

    使用这样的技巧:

    void multiply(unsigned& rhi, unsigned& rlo, unsigned a, unsigned b)
    {
    __asm__(
    "    mull  %[b]\n"
    :"=d"(rhi),"=a"(rlo)
    :"1"(a),[b]"rm"(b));
    }
    

    注意输入操作数 a"1" 参数规范 . 这意味着"put 'a' into the same place where argument #1 is" .

  • 0

    Brett Hale的answer在某些情况下产生次优代码(至少在GCC 5.4.0上) .

    鉴于:

    static inline void mulq(uint64_t *high, uint64_t *low, uint64_t x, uint64_t y) {
        __asm__ ("mulq %3" : "=a" (*low), "=d" (*high) : "%0" (x), "rm" (y) : "cc");
    }
    
    uint64_t foo();
    

    然后 mulq(&high, &low, foo(), 42) 编译为:

    call    foo
        movl    $42, %edx
        mulq    %rdx
    

    ......这是最佳的 .

    但现在颠倒操作数的顺序:

    mulq(&high, &low, 42, foo());
    

    ...看看编译代码会发生什么:

    call    foo
        movq    %rax, %rdx
        movl    $42, %eax
        mulq    %rdx
    

    哎呀!发生了什么?编译器坚持在 rax 中放入42,因此它必须将 foo() 的返回值移出 rax . 显然, % (可交换)操作数约束是有缺陷的 .

    有没有办法优化这个?事实证明,虽然它有点混乱 .

    static inline void mulq(uint64_t *high, uint64_t *low, uint64_t x, uint64_t y) {
        __asm__ (
            ".ifnc %2,%%rax\n\t"
            "mulq %2\n\t"
            ".else\n\t"
            "mulq %3\n\t"
            ".endif"
            : "=a,a" (*low), "=d,d" (*high)
            : "a,rm" (x), "rm,a" (y)
            : "cc");
    }
    

    现在 mulq(&high, &low, foo(), 42) 编译为:

    call    foo
        movl    $42, %edx
        .ifnc   %rax,%rax
        mulq    %rax
        .else
        mulq    %rdx
        .endif
    

    并且 mulq(&high, &low, 42, foo()) 编译为:

    call    foo
        movl    $42, %edx
        .ifnc   %rdx,%rax
        mulq    %rdx
        .else
        mulq    %rax
        .endif
    

    此代码使用汇编程序技巧来解决GCC不允许我们发出不同汇编代码的限制,具体取决于它所选择的约束 . 在每种情况下,汇编器将只发出两个可能的 mulq 指令中的一个,具体取决于编译器是否选择将 xy 放在 rax 中 .

    可悲的是,如果我们这样做,这个技巧是不理想的将 foo() 的返回值乘以内存位置的值:

    extern uint64_t bar;
    

    现在 mulq(&high, &low, bar, foo()) 编译为:

    call    foo
        .ifnc bar(%rip),%rax
        mulq bar(%rip)
        .else
        mulq %rax
        .endif
    

    ...这是最佳的,但 mulq(&high, &low, foo(), bar) 编译为:

    movq    bar(%rip), %rbx
        call    foo
        .ifnc   %rax,%rax
        mulq    %rax
        .else
        mulq    %rbx
        .endif
    

    ......不必要地将 bar 复制到 rbx .

    遗憾的是,我无法在所有情况下找到使GCC输出最佳代码的方法 . 为了调查,强制乘法器为内存操作数,只会导致GCC将 bar(%rip) 加载到寄存器中,然后将该寄存器存储到临时堆栈位置,然后传递给 mulq .

相关问题