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

sta*_*ufk 8 gcc 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)    
        );
}
Run Code Online (Sandbox Code Playgroud)

此代码是正确的,但不是最佳的.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)    
    );
Run Code Online (Sandbox Code Playgroud)

不幸的是,这是错误的.如果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;
}
Run Code Online (Sandbox Code Playgroud)

编译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
Run Code Online (Sandbox Code Playgroud)

"mul%rax"应为"mul%rdx".

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

Cas*_*eri 5

2012年的问题在2019年仍然非常重要。尽管gcc发生了变化,并且生成的某些代码在2012年并不是最优的,但是现在,反之亦然。

通过激发维特洛克的分析,我测试mulq9种不同的情况下,其中每个的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; }
Run Code Online (Sandbox Code Playgroud)

我已经测试了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");
}
Run Code Online (Sandbox Code Playgroud)

在所有情况下,所有实现仍然无法生成最佳代码。他人无法为h3, h4和生成最佳代码时h6,惠特洛克和我的仅因h3以下原因而失败:

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

在其他所有条件都相同的情况下,可以看到我的东西比惠特洛克的东西更简单。通过额外的间接级别和使用gcc的内置函数(也可以在clang中使用,但我尚未测试),可以h3通过调用此函数而不是来获得最佳效果mulq

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);
}
Run Code Online (Sandbox Code Playgroud)

产量:

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

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

模板内无法确定选择了哪个替代方案。但是,您可以使用诸如__builtin_constant_p之类的内置函数包装asm语句,以实现所需的结果。

Compiler Explorer自行查看。

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


Bre*_*ale 4

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

longlong.h这与您在各种 GNU 软件包中发现的类似;"r,m"而不是"rm"真正为了clang的利益。多重约束语法对于 clang 来说仍然很重要,如此处所讨论。这很遗憾,但我仍然发现 clang 在约束匹配方面(尤其是在 x86[-86] 上)比 gcc 做得更差。对于海湾合作委员会:

__asm__ ("mulq %3" : "=a" (*low), "=d" (*high) : "%0" (x), "rm" (y))
Run Code Online (Sandbox Code Playgroud)

就足够了,并且有利于保留(y)在寄存器中,除非寄存器压力太高;但在很多情况下,clang似乎总是会溢出。我的测试表明它将选择"r"多重约束语法中的第一个选项。

"%3"作为指令中的被乘数,允许寄存器(首选)或内存位置,如第三个操作数的别名,相对于零,即(y)"0"别名“第零”操作数:(*low),它是明确的"a",即%rax对于 64 位。%其中的主角是"%0"交换运算符:也就是说,如果有助于寄存器分配,则 (x) 可以与 (y) 交换。显然,mulq是可交换的:x * y == y * x

我们实际上在这里受到很大限制。mulq将 64 位操作数乘以%3in 的值%rax以生成 128 位乘积:%rdx:%rax。这"0" (x)意味着(x)必须加载到%rax,并且(y)必须加载到 64 位寄存器或内存地址中。然而%0意味着(x), 并且后面的输入(y)可以交换。

我还会参考我找到的最佳实用内联汇编教程。虽然gcc参考文献是“权威的”,但它们是一个糟糕的教程。


谢克里斯发现我原来的约束排序中的错误。