繁体   English   中英

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

[英]Can GCC emit different instruction mnemonics when choosing between multiple alternative operand constraints of inline assembly?

我正在尝试为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有一个这样的语法,称为“多个替代约束”。 注意逗号(但是整个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,以便在每种情况下生成正确的输出?

这个2012年的问题在2019年仍然非常相关。尽管gcc已经发生了变化,并且在2012年产生的一些代码并不是最优的,但是现在,反之亦然。

通过激发维特洛克的分析,我测试mulq在9个不同的情况下,每一个xy要么是一个常数( 56 ),或在存储器中的值( barzar ),或在一个值raxf1()f2() ):

uint64_t h1() { uint64_t h, l; mulq(&h, &l,    5,    6); return h + l; }
uint64_t h2() { uint64_t h, l; mulq(&h, &l,    5,  bar); return h + l; }
uint64_t h3() { uint64_t h, l; mulq(&h, &l,    5, f1()); return h + l; }
uint64_t h4() { uint64_t h, l; mulq(&h, &l,  bar,    5); return h + l; }
uint64_t h5() { uint64_t h, l; mulq(&h, &l,  bar,  zar); return h + l; }
uint64_t h6() { uint64_t h, l; mulq(&h, &l,  bar, f1()); return h + l; }
uint64_t h7() { uint64_t h, l; mulq(&h, &l, f1(),    5); return h + l; }
uint64_t h8() { uint64_t h, l; mulq(&h, &l, f1(),  bar); return h + l; }
uint64_t h9() { uint64_t h, l; mulq(&h, &l, f1(), f2()); return h + l; }

我测试了5个实现: StaufkWhitlockHaleBurdo和我自己:

inline void mulq(uint64_t *high, uint64_t *low, uint64_t x, uint64_t y) {
    asm("mulq %[y]" : [a]"=a,a"(*low), "=d,d"(*high) : "%a,rm"(x), [y]"rm,a"(y) : "cc");
}

在所有情况下,所有实现仍然无法生成最佳代码。 虽然其他人未能为h3, h4h6生成最佳代码,但Whitlock和我的仅为h3失败:

h3():
 callq 4004d0 <f1()>
 mov %rax,%r8
 mov $0x5,%eax
 mul %r8
 add %rdx,%rax
 retq 

在其他条件相同的情况下,人们可以看出我的比Whitlock更简单。 使用额外的间接级别并使用gcc的内置函数(也可以在clang中使用但我还没有测试过)可以通过调用此函数而不是mulq来获得最佳h3

inline void mulq_fixed(uint64_t* high, uint64_t* low, uint64_t x, uint64_t y) {
    if (__builtin_constant_p(x))
        mulq(high, low, y, x);
    else
        mulq(high, low, x, y);
}

收益率:

h3():
 callq 4004d0 <f1()>
 mov $0x5,%edx
 mul %rdx
 add %rdx,%rax
 retq 

使用__builtin_constant_p的想法实际上来自gcc的doc:

模板中无法确定选择了哪种替代方案。 但是,您可以使用__builtin_constant_p等内置函数包装asm语句,以获得所需的结果。

Compiler Explorer中自己查看。

注意:Whitlock实现的另一个更小且意想不到的缺点。 您需要在编译器资源管理器中检查选项11010 ,否则输出会产生误导,函数h1 ,..., h9似乎使用指令mulq两次。 这是因为编译器Explorer的解析器不处理汇编指令.ifnc / .else / .endif 正确并简单地将其删除,显示两种可能路径( .if 'S和.else的)。 或者,您可以取消选中.text

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

这类似于你在longlong.h找到的各种GNU包中包含的内容; "r,m"而不是"rm"真的是为了铿锵有利。 多重约束语法仍然似乎是铛重要的,因为讨论在这里 这是一种耻辱,但我仍然发现clang在约束匹配(尤其是在x86 [-86]上)比gcc更糟糕。 对于gcc:

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

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

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

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

我还会参考我发现的最好的实用内联汇编教程 虽然gcc引用是“权威的”,但它们是一个糟糕的教程。


感谢Chris在原始约束排序中获取错误。

Brett Hale的答案在某些情况下会产生次优代码(至少在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

哎呀! 发生了什么? 编译器坚持将42放在rax ,因此它必须将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

与关于内联asm语法的一般问题分开:

实际上,对于64x64 => 128位乘法,实际上不需要内联asm
GCC / clang / ICC知道如何将a * (unsigned __int128)b优化为单个mul指令。 鉴于两个GNU C扩展(内联asm与__int128 )之间的选择,如果你可以让编译器自己发出漂亮的asm,总是避免使用内联asm。 https://gcc.gnu.org/wiki/DontUseInlineAsm

unsigned __int128 foo(unsigned long a, unsigned long b) {
    return a * (unsigned __int128)b;
}

在Godbolt编译器资源管理器上编译gcc / clang / ICC

# gcc9.1 -O3  x86-64 SysV calling convention
foo(unsigned long, unsigned long):
        movq    %rdi, %rax
        mulq    %rsi
        ret                         # with the return value in RDX:RAX

或者返回高半部分

unsigned long umulhi64(unsigned long a, unsigned long b) {
    unsigned __int128 res = a * (unsigned __int128)b;
    return res >> 64;
}

        movq    %rdi, %rax
        mulq    %rsi
        movq    %rdx, %rax
        ret

GCC完全理解这里发生了什么,并且*是可交换的,所以它可以使用任一输入作为内存操作数,如果它只有一个在寄存器而不是另一个。

AFAIK不幸的是,根据来自寄存器或内存的一些输入,通常不可能使用不同的asm模板。 因此,完全使用不同的策略(例如直接加载到SIMD寄存器而不是做整数)是不可能的。

多替代约束事物非常有限,主要仅适用于像add这样的指令的内存源与内存目标版本。

使用这样的技巧:

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"参数规范。 这意味着“将'放入'参数#1所在的同一个地方”。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM