我正在尝试为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,以便在每种情况下生成正确的输出?
答案 0 :(得分:5)
这个2012年的问题在2019年仍然非常重要。尽管gcc发生了变化,并且生成的某些代码在2012年并不是最优的,但是现在,反之亦然。
受Whitlock分析的启发,我在9种不同的情况下对mulq
进行了测试,其中x
和y
均为常数({{1} },5
)或内存中的值(6
,bar
)或zar
中的值(rax
,f1()
):< / p>
f2()
我已经测试了5种实现:Staufk,Whitlock,Hale,Burdo和我自己的实现:
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; }
在所有情况下,所有实现仍然无法生成最佳代码。尽管其他人无法为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,
和h4
生成最佳代码,但惠特洛克和我的人仅对h6
失败:
h3
在其他所有条件都相同的情况下,可以看到我的比惠特洛克的更简单。通过额外的间接级别和使用gcc的内置函数(也可以在clang中使用,但我尚未测试),可以通过调用此函数而不是h3():
callq 4004d0 <f1()>
mov %rax,%r8
mov $0x5,%eax
mul %r8
add %rdx,%rax
retq
来获得最佳的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);
}
使用h3():
callq 4004d0 <f1()>
mov $0x5,%edx
mul %rdx
add %rdx,%rax
retq
的想法实际上来自gcc的文档:
模板内无法确定选择了哪个替代方案。但是,您可以使用诸如__builtin_constant_p之类的内置函数包装asm语句,以实现所需的结果。
在Compiler Explorer中亲自查看。
注意:Whitlock的实现还有另一个较小而出乎意料的缺点。您需要检查Compiler Explorer中的选项 11010 ,否则输出会产生误导,并且功能__builtin_constant_p
,...,h1
似乎使用了指令h9
两次。这是因为Compiler Explorer的解析器不处理汇编器伪指令mulq
/ .ifnc
/ .else
properly并只是删除它们,显示了两种可能的路径(.endif
的和.if
的)。或者,您可以取消选中选项 .text 。
答案 1 :(得分:3)
与关于内联asm语法的一般问题分开:
对于64x64 => 128位乘法,您实际上不需要内联汇编。。
GCC / clang / ICC知道如何将a * (unsigned __int128)b
优化为一条mul
指令。给定两个GNU C扩展之间的选择(内联汇编与__int128
),如果可以让编译器自己发出漂亮的汇编,请始终避免内联汇编。 https://gcc.gnu.org/wiki/DontUseInlineAsm
unsigned __int128 foo(unsigned long a, unsigned long b) {
return a * (unsigned __int128)b;
}
在gcc / clang / ICC上编译为on the Godbolt compiler explorer
# 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完全了解这里发生的情况,并且*
是可交换的,因此,如果寄存器中只有一个而不是另一个,则它可以将其中一个输入用作内存操作数。
不幸的是,根据某些来自寄存器或存储器的输入,通常无法使用不同的asm模板。因此,不可能完全使用其他策略(例如,直接加载到SIMD寄存器中而不是执行整数操作)。
多重替代约束条件非常有限,主要仅适用于诸如add
之类的指令的内存源版本和内存目标版本。
答案 2 :(得分:2)
__asm__ ("mulq %3" : "=a,a" (*low), "=d,d" (*high) : "%0,0" (x), "r,m" (y))
这与您在各种GNU包中包含的longlong.h
中的内容类似; "r,m"
而不是"rm"
真的是为了铿锵利益。对于clang,多重约束语法似乎仍然很重要,如here所述。这是一种耻辱,但我仍然发现clang在约束匹配(尤其是在x86 [-86]上)比gcc更糟糕。对于gcc:
__asm__ ("mulq %3" : "=a" (*low), "=d" (*high) : "%0" (x), "rm" (y))
就足够了,并且有利于将(y)
保留在寄存器中,除非注册压力太高;但clang 总是似乎在很多情况下都会泄漏。我的测试显示它将在多重约束语法中选择第一个选项"r"
。
"%3"
作为指令中的被乘数,允许寄存器(偏好)或内存位置,由第三个操作数别名相对为零,即(y)
。 "0"
别名为“零”操作数:(*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引用是“权威的”,但它们却是一个糟糕的教程。
感谢Chris在原始约束排序中拾取错误。
答案 3 :(得分:1)
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
糟糕!发生了什么?编译器坚持将{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
指令中的一个,具体取决于编译器是选择将x
还是y
放入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
。 / p>
答案 4 :(得分:0)
使用这样的技巧:
void multiply(unsigned& rhi, unsigned& rlo, unsigned a, unsigned b)
{
__asm__(
" mull %[b]\n"
:"=d"(rhi),"=a"(rlo)
:"1"(a),[b]"rm"(b));
}
注意输入操作数"1"
的{{1}}参数规范。
这意味着“把'放'到论点#1所在的同一个地方。”