您不需要也不应该为此使用内联汇编。编译器甚至可以比smull 做得更好,并使用smlal 与一条指令进行乘法累加:
int64_t accum(int64_t acc, int32_t x, int32_t y) {
return acc + x * (int64_t)y;
}
将 (with gcc8.2 -O3 -mcpu=arm10e on the Godbolt compiler explorer) 编译成这个 asm:(ARM10E 是我选择的一个 ARMv5 微架构 from Wikipedia's list)
accum:
smlal r0, r1, r3, r2 @, y, x
bx lr @
作为奖励,这个纯 C 也可以高效地为 AArch64 编译。
https://gcc.gnu.org/wiki/DontUseInlineAsm
如果你坚持使用内联 asm:
或者在其他说明的一般情况下,您可能会想要这个。
首先,请注意smull 输出寄存器不允许与第一个输入寄存器重叠,因此您必须将此告知编译器。 输出操作数的早期破坏约束( s) 会告诉编译器它不能在这些寄存器中输入。我没有看到一种干净的方法来告诉编译器第二个输入可以与输出在同一个寄存器中。
在 ARMv6 及更高版本中取消了此限制(请参阅 this Keil documentation)“Rn 必须不同于 ARMv6 之前架构中的 RdLo 和 RdHi”,但为了 ARMv5 兼容性,您需要确保编译器填写 inline-asm 模板时不会违反此规定。
当面向 32 位平台时,优化编译器可以优化掉将 32 位 C 变量组合成 64 位 C 变量的移位/或。它们已经将 64 位变量存储为一对寄存器,并且在正常情况下可以找出在 asm 中没有实际工作要做。
因此您可以将 64 位输入或输出指定为一对 32 位变量。
#include <stdint.h>
int64_t testFunc(int64_t acc, int32_t x, int32_t y)
{
uint32_t prod_lo, prod_hi;
asm("SMULL %0, %1, %2, %3"
: "=&r" (prod_lo), "=&r"(prod_hi) // early clobber for pre-ARMv6
: "r"(x), "r"(y)
);
int64_t prod = ((int64_t)prod_hi) << 32;
prod |= prod_lo; // + here won't optimize away, but | does, with gcc
return acc + prod;
}
不幸的是,early-clobber 意味着我们总共需要 6 个寄存器,但 ARM 调用约定只有 6 个 call-clobbered 寄存器 (r0..r3, lr, and ip (aka r12))。其中之一是 LR,它有返回地址,所以我们不能丢失它的值。内联到已经保存/恢复多个寄存器的常规函数时可能没什么大不了的。
再次from Godbolt:
@ gcc -O3 output with early-clobber, valid even before ARMv6
testFunc:
str lr, [sp, #-4]! @, Save return address (link register)
SMULL ip, lr, r2, r3 @ prod_lo, prod_hi, x, y
adds r0, ip, r0 @, prod, acc
adc r1, lr, r1 @, prod, acc
ldr pc, [sp], #4 @ return by popping the return address into PC
@ gcc -O3 output without early-clobber (&) on output constraints:
@ valid only for ARMv6 and later
testFunc:
SMULL r3, r2, r2, r3 @ prod_lo, prod_hi, x, y
adds r0, r3, r0 @, prod, acc
adc r1, r2, r1 @, prod, acc
bx lr @
或者您可以使用"=r"(prod64) 约束并使用修饰符来选择您获得%0 的哪一半。不幸的是,出于某种原因,gcc 和 clang 发出的 asm 效率较低,从而节省了更多的寄存器 (并保持 8 字节堆栈对齐)。 gcc 用 2 代替 1,clang 用 4 代替 2。
// using an int64_t directly with inline asm, using %Q0 and %R0 constraints
// Q is the low half, R is the high half.
int64_t testFunc2(int64_t acc, int32_t x, int32_t y)
{
int64_t prod; // gcc and clang seem to want more free registers this way
asm("SMULL %Q0, %R0, %1, %2"
: "=&r" (prod) // early clobber for pre-ARMv6
: "r"(x), "r"(y)
);
return acc + prod;
}
再次使用 gcc -O3 -mcpu=arm10e 编译。 (clang保存/恢复4个寄存器)
@ gcc -O3 with the early-clobber so it's safe on ARMv5
testFunc2:
push {r4, r5} @
SMULL r4, r5, r2, r3 @ prod, x, y
adds r0, r4, r0 @, prod, acc
adc r1, r5, r1 @, prod, acc
pop {r4, r5} @
bx lr @
因此,出于某种原因,使用当前 gcc 和 clang 手动处理 64 位整数的一半似乎更有效。这显然是一个错过的优化错误。