10

高级编程语言通常提供一个函数来确定浮点值的绝对值。例如,在 C 标准库中,就有fabs(double)函数。

这个库函数实际上是如何为 x86 目标实现的?当我调用这样的高级函数时,“幕后”实际上会发生什么?

这是一个昂贵的操作(乘法和取平方根的组合)?还是仅仅通过删除内存中的负号找到结果?

4

1 回答 1

15

一般来说,计算浮点数的绝对值是一种极其便宜和快速的操作。

在几乎所有情况下,您都可以简单地将fabs标准库中的函数视为一个黑匣子,在必要时将其散布在您的算法中,而无需担心它会如何影响执行速度。

如果您想了解为什么这是一个如此便宜的操作,那么您需要了解一点关于浮点值是如何表示的。尽管 C 和 C++ 语言标准实际上并没有强制要求,但大多数实现都遵循IEEE-754标准。在该标准中,每个浮点值的表示形式都包含一个称为符号位的位,这标志着该值是正数还是负数。例如,考虑一个double64 位双精度浮点值

双精度浮点值的位级表示
     (图片由 Codekaizen 提供,通过 Wikipedia,获得 CC-bySA 许可。)

你可以看到最左边的标志位,浅蓝色。这适用于 IEEE-754 中浮点值的所有精度。因此,取绝对值基本上只是在内存中的值表示中翻转一个字节。特别是,您只需要屏蔽符号位(按位与),将其强制为 0,即无符号位。

假设您的目标体系结构具有对浮点​​运算的硬件支持,这通常是一个单一的、一个周期的指令——基本上,尽可能快。优化编译器将内联对fabs库函数的调用,在其位置发出单个硬件指令。

如果您的目标架构没有对浮点的硬件支持(这在当今非常罕见),那么将有一个库在软件中模拟这些语义,从而提供浮点支持。通常,浮点仿真很慢,但找到绝对值是您可以做的最快的事情之一,因为它实际上只是在进行一点操作。您将支付对 的函数调用的开销fabs,但在最坏的情况下,该函数的实现将只涉及从内存中读取字节、屏蔽符号位并将结果存储回内存。

特别看一下 x86,它确实在硬件中实现了 IEEE-754,C 编译器有两种主要方式将调用转换fabs为机器代码。

在 32 位版本中,旧版 x87 FPU用于浮点运算,它将发出fabs指令。(是的,与 C 函数同名。)这会从 x87 寄存器堆栈顶部的浮点值中去除符号位(如果存在)。在 AMD 处理器和 Intel Pentium 4 上,fabs是具有 2 周期延迟的 1 周期指令。在 AMD Ryzen 和所有其他 Intel 处理器上,这是一条 1 周期指令,具有 1 周期延迟。

在可以假设支持 SSE 的 32 位构建中,以及在所有64 位构建(始终支持 SSE)中,编译器将发出一条ANDPS指令*完全按照我上面描述的方式执行:它对浮点值进行按位与运算使用常量掩码,屏蔽符号位。请注意,SSE2 没有像 x87 那样获取绝对值的专用指令,但它甚至不需要一个,因为多功能按位运算指令可以很好地完成这项工作。执行时间(周期、延迟等特性)从一个处理器微架构到另一个处理器微架构的差异更大,但它通常具有 1-3 个周期的吞吐量,具有相似的延迟。如果您愿意,可以在Agner Fog 的说明表中查找对于感兴趣的处理器。

如果你真的有兴趣深入研究,你可能会看到这个答案(Peter Cordes 的帽子提示),它探索了使用 SSE 指令实现绝对值函数的各种不同方法,比较它们的性能并讨论你如何能获取编译器以生成适当的代码。如您所见,由于您只是在操作位,因此有多种可能的解决方案!但在实践中,当前的编译器完全按照我为 C 库函数所描述的那样做fabs,这是有道理的,因为这是最好的通用解决方案。

__
*从技术上讲,这也可能是ANDPD,其中的D意思是“双”(以及S“单”),但ANDPD需要 SSE2 支持。SSE 支持单精度浮点运算,并且一直可用到 Pentium III。双精度浮点运算需要 SSE2,它是在 Pentium 4 中引入的。x86-64 CPU始终支持 SSE2。是否ANDPS使用ANDPD是由编译器的优化器做出的决定;有时您会看到ANDPS在双精度浮点值上使用它,因为它只需要以正确的方式编写掩码。
此外,在支持 AVX 指令的 CPU 上,您通常会在ANDPS/ANDPD指令,使其变为VANDPS/ VANDPD。可以在网上其他地方找到有关其工作原理及其用途的详细信息;只需说混合 VEX 和非 VEX 指令会导致性能损失,因此编译器会尽量避免它。不过,这两个版本同样具有相同的效果和几乎相同的执行速度。

哦,因为 SSE 是一个SIMD指令集,所以可以一次计算多个浮点值的绝对值。正如您可能想象的那样,这特别有效。具有自动矢量化功能的编译器将尽可能生成这样的代码。示例(掩码可以即时生成,如此处所示,也可以作为常量加载):

cmpeqd xmm1, xmm1     ; generate the mask (all 1s) in a temporary register
psrld  xmm1, 1        ; put 1s in but the left-most bit of each packed dword
andps  xmm0, xmm1     ; mask off sign bit in each packed floating-point value

于 2017-06-23T09:34:12.733 回答