GNU C inline asm 设计为在 asm 语句的开始/结束时不需要数据移动指令。每当您编写 mov 或 fld 或作为内联汇编中的第一条指令时,您就违背了约束系统的目的。您应该首先要求编译器将数据放在您想要的位置。
另外,在 2016 年编写新的 x87 代码通常是在浪费时间。这很奇怪,并且与进行 FP 数学(xmm 寄存器中的标量或向量指令)的正常方式有很大不同。如果它针对非常不同的微体系结构进行了手动调整,或者没有利用 SSE 指令,那么通过将古老的 asm 代码翻译成纯 C 语言,您可能会获得更好的结果。如果您仍想编写 x87 代码,请参阅x86 标签 wiki 中的指南。
If you're trying to learn asm using GNU C inline asm, just don't. Pick any other way to learn asm, e.g. writing whole functions and calling them from C. 另请参阅该答案的底部以获取编写良好 GNU C 内联汇编的链接集合。
有special rules for x87 floating-point operands,因为 x87 寄存器堆栈不是随机访问的。这使得 inline-asm 比“正常”的东西更难使用。 获得最佳代码似乎也比正常更难。
在我们的例子中,我们知道我们需要在 FP 堆栈顶部有一个输入操作数,并在那里产生我们的结果。要求编译器为我们执行此操作意味着我们不需要 fadd 之外的任何指令。
asm (
"fadd %[num2]\n\t"
: "=t" (fsum) // t is the top of the register stack
: [num1] "%0" (num1), [num2] "f" (num2) // 0 means same reg as arg 0, and the % means they're commutative. gcc doesn't allow an input and output to both use "t" for somre reason. For integer regs, naming the same reg for an input and an output works, instead of using "0".
: // "st(1)" // we *don't* pop the num2 input, unlike the FYL2XP1 example in the gcc manual
// This is optimal for this context, but in other cases faddp would be better
// we don't need an early-clobber "=&t" to prevent num2 from sharing a reg with the output, because we already have a "0" constraint
);
有关%0 的说明,请参阅the docs for constraint modifiers。
在fadd 之前:num2 是%st(0)。 num1 要么在内存中,要么在另一个 FP 堆栈寄存器中。编译器选择哪个,填写寄存器名或有效地址。
这应该有望让编译器在正确的次数之后弹出堆栈。 (请注意,当输出约束必须是 FP 堆栈寄存器时,fst %0 非常愚蠢。它很可能最终成为像 fst %st(0) 这样的空操作。)
如果两个 FP 值都已经在 %st 寄存器中,我看不到一个简单的方法来优化它以使用 faddp。例如如果 num1 之前在 %st1 中,faddp %st1 将是理想的,但在 FP 寄存器中仍然不需要。
这是一个实际编译的完整版本,甚至可以在 64 位模式下工作,因为我为您编写了一个类型双关语包装函数。任何将 FP 寄存器中的 FP args 传递给 varargs 函数的 ABI 都需要这样做。
#include <stdio.h>
#include <stdint.h>
uint32_t pun(float x) {
union fp_pun {
float single;
uint32_t u32;
} xu = {x};
return xu.u32;
}
int main()
{
float num1, num2, fsum;
printf("Enter two floating point numbers: \n");
scanf("%f %f", &num1, &num2);
asm (
"fadd %[num2]\n\t"
: "=t" (fsum)
: [num1] "%0" (num1), [num2] "f" (num2) // 0 means same reg as arg 0, and the % means it's commutative with the next operand. gcc doesn't allow an input and output to both use "t" for some reason. For integer regs, naming the same reg for an input and an output works, instead of using "0".
: // "st(1)" // we *don't* pop the num2 input, unlike the FYL2XP1 example in the gcc manual
// This is optimal for this context, but in other cases faddp would be better
// we don't need an early-clobber "=&t" to prevent num2 from sharing a reg with the output, because we already have a "0" constraint
);
printf("The sum of %f and %f is %f\n", num1, num2, fsum);
// Use a union for type-punning. The %a hex-formatted-float only works for double, not single
printf("The hex equivalent of the numbers and sum is %#x + %#x = %#x\n",
pun(num1), pun(num2), pun(fsum));
return 0;
}
在Godbolt Compiler Explorer 上查看它是如何编译的。
取出-m32,看看在使用 SSE 进行 FP 数学运算的普通代码中,仅一次添加就将数据放入 x87 寄存器是多么愚蠢。 (尤其是因为在scanf 为我们提供单精度之后,它们还必须为printf 转换为双精度。)
gcc 最终也为 32 位生成了一些看起来非常低效的 x87 代码。它最终在 regs 中有两个 args,因为它从单精度加载它以准备存储为双精度。出于某种原因,它复制了 FP 堆栈上的值,而不是在执行 fadd 之前存储为双精度值。
所以在这种情况下,"f" 约束比"m" 约束生成更好的代码,而且我没有看到使用 AT&T 语法为内存操作数指定单精度操作数大小而不破坏 asm 的简单方法用于寄存器操作数。 (fadds %st(1) 不汇编,但 fadd (mem) 也不与 clang 汇编。显然,GNU 默认为单精度内存操作数。)使用 Intel 语法,修改后的操作数大小附加到内存操作数,如果编译器选择一个内存操作数就会出现,否则不会出现。
无论如何,这个序列会比 gcc 发出的更好,因为它避免了 fld %st(1):
call __isoc99_scanf
flds -16(%ebp)
subl $12, %esp # make even more space for args for printf beyond what was left after scanf
fstl (%esp) # (double)num1
flds -12(%ebp)
fstl 8(%esp) # (double)num2
faddp %st(1) # pops both inputs, leaving only fsum in %st(0)
fsts -28(%ebp) # store the single-precision copy
fstpl 16(%esp) # (double)fsum
pushl $.LC3
call printf
但 gcc 显然不打算这样做。编写内联 asm 以使用 faddp 使 gcc 在 faddp 之前执行额外的 fld %st(1) 而不是说服它在添加之前为 printf 存储 double 参数。
如果设置了单精度存储,则更好的是它们可以作为类型双关语 printf 的参数,而不必为此再次复制。如果手动编写函数,我会让 scanf 将结果存储到用作 printf 参数的位置。