如何使用扩展的 gcc 程序集指定 x87 FPU 堆栈的破坏底部?

How to specify clobbered bottom of the x87 FPU stack with extended gcc assembly?

在我们的代码库中,我发现了这个片段,用于在 x87 上快速、朝向负无穷大1 舍入:

inline int my_int(double x)
{
  int r;
#ifdef _GCC_
  asm ("fldl %1\n"
       "fistpl %0\n"
       :"=m"(r)
       :"m"(x));
#else
  // ...
#endif
  return r;
}

我不是很熟悉 GCC 扩展汇编语法,但根据我从文档中收集到的信息:

现在,回到我的问题:FPU 堆栈最终确实是平衡的,但是如果所有 8 个位置都已被使用并且我正在溢出怎么办?编译器怎么知道它不能相信 ST(7) 在它离开的地方?是否应该添加一些破坏?

Edit 我试图在 clobber 列表中指定 st(7),它似乎影响了代码生成器,现在我'我们将等待对这一事实的一些确认。


附带说明:在 glibc 和 MinGW 中查看准系统 lrint 的实现,我看到类似

的内容
__asm__ __volatile__ ("fistpl %0"
                      : "=m" (retval)
                      : "t" (x)
                      : "st");

我们要求将输入直接放在 ST(0) 中(这避免了可能无用的 fldl); "st" 是什么东西?文档似乎只提到 t (即堆栈的顶部)。


  1. 是的,这取决于当前的舍入模式,在我们的应用程序中应该始终是 "towards negative infinity"。

looking at the implementation of the barebones lrint both in glibc and in MinGW I see something like

__asm__ __volatile__ ("fistpl %0"
                     : "=m" (retval)
                     : "t" (x)
                     : "st");

where we are asking for the input to be placed directly in ST(0) (which avoids that potentially useless fldl)

这实际上是将您想要的代码表示为内联汇编的正确方法。

要生成尽可能最佳的代码,您需要使用输入和输出。与其硬编码必要的 load/store 指令,不如让编译器生成它们。这不仅引入了消除潜在不必要指令的可能性,还意味着编译器可以在需要时更好地 安排 这些指令(也就是说,它可以将指令交错在先前的代码序列,通常将其成本降至最低)。

what is that "st" clobber? The docs seems to mention only t (i.e. the top of the stack).

"st" clobber 指的是 st(0) 寄存器,,x87 FPU 堆栈的顶部。 Intel/MASM表示法称为st(0),AT&T/GAS表示法一般简称为st。并且,根据 GCC 的 clobbers 文档,破坏列表中的项目是 "either register names or the special clobbers"("cc"(条件 codes/flags)和 "memory")。所以这只是意味着内联汇编破坏(覆盖)了 st(0) 寄存器。这个破坏是必要的原因是 fistpl 指令弹出堆栈的顶部,从而破坏 st(0).

的原始内容

关于此代码,我唯一关心的是文档中的以下段落:

Clobber descriptions may not in any way overlap with an input or output operand. For example, you may not have an operand describing a register class with one member when listing that register in the clobber list. Variables declared to live in specific registers (see Explicit Register Variables) and used as asm input or output operands must have no part mentioned in the clobber description. In particular, there is no way to specify that input operands get modified without also specifying them as output operands.

When the compiler selects which registers to use to represent input and output operands, it does not use any of the clobbered registers. As a result, clobbered registers are available for any use in the assembler code.

如您所知,t constraint 表示 x87 FPU 堆栈的顶部。问题是,这与 st 寄存器相同,并且文档非常清楚地表明我们不能有一个 clobber 将同一寄存器指定为 input/output 操作数之一。此外,由于文档指出编译器被禁止使用任何被破坏的寄存器来表示 input/output 操作数,因此此内联汇编提出了一个不可能的请求——将此值加载到 x87 FPU 堆栈的顶部而不将其放入st!

现在,我假设 glibc 的作者知道他们在做什么,并且比您或我更熟悉编译器对内联汇编的实现,所以这段代码 可能合法合法。

实际上,x87 的类堆栈寄存器的不寻常情况似乎强制了 clobber 和操作数之间的正常交互的异常。 official documentation 表示:

On x86 targets, there are several rules on the usage of stack-like registers in the operands of an asm. These rules apply only to the operands that are stack-like registers:

  1. Given a set of input registers that die in an asm, it is necessary to know which are implicitly popped by the asm, and which must be explicitly popped by GCC.

    An input register that is implicitly popped by the asm must be explicitly clobbered, unless it is constrained to match an output operand.

这完全符合我们的情况。

the official documentation(链接部分底部)中的示例提供了进一步的确认:

This asm takes two inputs, which are popped by the fyl2xp1 opcode, and replaces them with one output. The st(1) clobber is necessary for the compiler to know that fyl2xp1 pops both inputs.

asm ("fyl2xp1" : "=t" (result) : "0" (x), "u" (y) : "st(1)");

在这里,clobber st(1) 与输入约束 u 相同,这似乎违反了上面引用的关于 clobbers 的文档,但使用和证明的原因与"st" 用作原始代码中的破坏者,因为 fistpl 弹出输入。


综上所述,现在您知道了如何在内联汇编中正确编写代码,我不得不回应之前的评论,他们建议 最佳 解决方案不是完全使用内联汇编。只需调用 lrint,它不仅具有您想要的确切语义,而且在某些情况下还可以被编译器更好地优化(例如,将其转换为单个 cvtsd2si指令,当目标架构支持SSE时)。