【问题标题】:How to specify clobbered bottom of the x87 FPU stack with extended gcc assembly?如何使用扩展 gcc 程序集指定 x87 FPU 堆栈的破坏底部?
【发布时间】:2016-09-27 15:18:42
【问题描述】:

在我们的代码库中,我发现了这个 sn-p 用于在 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 扩展汇编语法不是很熟悉,但从我从文档中收集到的内容来看:

  • r 必须是一个内存位置,我正在写回东西;
  • x 也必须是一个内存位置,数据的来源。
  • 没有 clobber 规范,因此编译器可以放心,在 sn-p 结束时,寄存器与他留下的一样。

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

编辑我试图在clobber列表中指定st(7),它似乎会影响代码生成,现在我将等待这个事实的一些确认。


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

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

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


  1. 是的,这取决于当前的舍入模式,在我们的应用程序中,该模式应始终“朝向负无穷大”。

【问题讨论】:

  • st 是相当于st0 或st(0) 的clobber
  • 内联汇编器很难理解和正确(仅在绝对必要时使用它),这已经够糟糕了。当您处理扩展内联汇编如何与 x87 FPU 堆栈交互的特殊性时,情况会更糟。使用 x86-64,您可以解决这个问题,因为您可以使用 SIMD 指令并在很大程度上避免与 x87 FPU 交互。
  • 您可能应该摆脱该内联汇编,并让 gcc 从具有所需语义的 C 中为您发出它。您绝对不想要求输入在内存中,因为如果它是计算的结果,那只会让自己陷入困境(所以它已经在 FP 堆栈的顶部)。让 gcc 发出代码将让它选择是 fist 还是 fistp,如果它仍然想要 FP 值。
  • @PeterCordes:实际上,那段代码引起了我的注意,当时我在做一些事后调试时,注意到一个直接的fstp/fldl 序列,这让我怀疑 gcc 是否已经消失疯狂的。 :-) 但是必须注意,这段代码有很长的路要走,它是 VC++ 6 中使用的 gcc/MinGW 中的“直接端口”(其中内联汇编几乎没有那么强大),并且该代码它本身来自一个更早的项目,它是因为臭名昭著的直系 int 在 x86 上的有害性能而编写的。
  • 嘿,是的,我从您的问题中得到的印象是代码的来源可疑。 :P 但是你问的是如何让它工作,而不是如何用可以可靠地编译成好的代码的东西来替换它。所以我觉得有必要指出这是可能的,至少如果你可以使用-fno-math-errno。

标签: c gcc assembly x86 x87


【解决方案1】:

在 glibc 和 MinGW 中查看准系统 lrint 的实现,我看到类似

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

我们要求将输入直接放在ST(0) 中(这样可以避免可能无用的fldl)

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

要生成尽可能最佳的代码,您需要利用输入和输出。与其硬编码必要的加载/存储指令,不如让编译器生成它们。这不仅引入了省略可能不必要的指令的可能性,还意味着编译器可以在需要时更好地调度这些指令(也就是说,它可以将指令交错在先前的代码序列中,通常会最大限度地降低成本)。

"st" 是什么东西?文档似乎只提到了t(即堆栈的顶部)。

"st" clobber 指的是st(0) 寄存器,即,x87 FPU 堆栈的顶部。 Intel/MASM 表示法称为st(0),AT&T/GAS 表示法通常简称为st。而且,根据 GCC 的 clobbers 文档,clobber 列表中的项目是“寄存器名称或特殊的clobbers”("cc"(条件代码/标志)和"memory")。所以这只是意味着内联程序集破坏(覆盖)st(0) 寄存器。之所以需要这个clobber,是因为fistpl指令弹出栈顶,从而破坏了st(0)的原始内容。

我对这段代码唯一担心的是文档中的以下段落:

Clobber 描述不能以任何方式与输入或输出操作数重叠。例如,在clobber 列表中列出该寄存器时,您可能没有描述具有一个成员的寄存器类的操作数。声明为存在于特定寄存器中的变量(请参阅Explicit Register Variables)并用作 asm 输入或输出操作数必须没有在 clobber 描述中提及的部分。特别是,如果不将输入操作数指定为输出操作数,就无法指定它们被修改。

当编译器选择使用哪些寄存器来表示输入和输出操作数时,它不会使用任何被破坏的寄存器。因此,被破坏的寄存器可用于汇编代码中的任何用途。

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

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

实际上,x87 的堆栈式寄存器的不寻常情况似乎迫使破坏者和操作数之间的正常交互出现异常。 official documentation 说:

在 x86 目标上,有几条关于在 asm 的操作数中使用类似堆栈的寄存器的规则。这些规则仅适用于类似于堆栈的寄存器的操作数:

  1. 给定一组在 asm 中死掉的输入寄存器,有必要知道哪些是 asm 隐式弹出的,哪些必须由 GCC 显式弹出。

    由 asm 隐式弹出的输入寄存器必须被显式破坏,除非它被限制为匹配输出操作数。

这完全符合我们的情况。

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

这个 asm 接受两个输入,由fyl2xp1 操作码弹出,并用一个输出替换它们。 st(1) clobber 是编译器知道fyl2xp1 弹出两个输入所必需的。

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

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


说了这么多,既然您知道如何正确地在内联汇编中编写代码,我必须回应以前的评论者,他们建议 最好的 解决方案是不要在以下位置使用内联汇编全部。只需调用lrint,它不仅具有您想要的确切语义,而且在某些情况下还可以由编译器更好地优化(例如,将其转换为单个cvtsd2si指令,当目标架构支持 SSE)。

【讨论】:

  • 感谢您抽出宝贵时间对我的老问题给出正确答案:-);关于同样在clobber列表中的输入寄存器的位似乎是关键,我稍后会尝试找出一些不同的情况(fchs => st0 中的输入,st0 中的输出,没有clobber;fdivp =>在 st0 和 st1 中输入,在 st0 中输出,弹出 st1;最重要的是,fxtract => 在 st0 中输入,在 st0 中输出 然后推送另一位输出),看看我是否'我仍然缺少一些东西。
猜你喜欢
  • 2013-11-22
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-01-02
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多