fabs(double) 如何在 x86 上实现?这是一项昂贵的手术吗?

How would fabs(double) be implemented on x86? Is it an expensive operation?

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

这个库函数实际上是如何为 x86 目标实现的?当我像这样调用高级函数时 "under the hood" 实际会发生什么?

这是一个昂贵的操作(乘法和取平方根的组合)吗?还是去掉内存中的一个负号就得到结果?

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

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

如果您想了解为什么这是一个如此便宜的操作,那么您需要了解一点浮点值的表示方式。虽然 C 和 C++ 语言标准实际上并没有强制要求,但大多数实现都遵循 IEEE-754 standard. In that standard, each floating-point value's representation contains a bit known as the sign bit, and this marks whether the value is positive or negative. For example, consider a double, which is a 64-bit double-precision floating-point value:


(图片由 Codekaizen 提供,来自维基百科,已获得 CC-bySA 许可。)

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

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

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

具体查看 x86,它确实在硬件中实现了 IEEE-754,您的 C 编译器主要通过两种方式将对 fabs 的调用转换为机器代码。

在 32 位版本中,其中 the legacy x87 FPU is being used for floating-point operations, it will emit an fabs instruction。 (是的,与 C 函数同名。)这会从 x87 寄存器堆栈顶部的浮点值中去除符号位(如果存在)。在 AMD 处理器和 Intel Pentium 4 上,fabs 是一个具有 2 个周期延迟的 1 个周期指令。在 AMD Ryzen 和所有其他 Intel 处理器上,这是一个具有 1 个周期延迟的 1 个周期指令。

在可以假定 SSE 支持的 32 位构建中,以及在 所有 64 位构建(始终支持 SSE)上,编译器将发出 ANDPS instruction* that does exactly what I described above: it bitwise-ANDs the floating-point value with a constant mask, masking out the sign bit. Notice that SSE2 doesn't have a dedicated instruction for taking the absolute value like x87 does, but that it doesn't even need one, because the multi-purpose bitwise-op instructions serve the job just fine. The execution time (cycles, latency, etc. characteristics) vary a bit more widely from one processor microarchitecture to another, but it generally has a throughput of 1–3 cycles, with a similar latency. If you like, you can look it up in Agner Fog's instruction tables 对于感兴趣的处理器。

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

__
* 从技术上讲,这也可能是 ANDPD,其中 D 表示 "double"(而 S 表示"single"),但 ANDPD 需要 SSE2 支持。 SSE 支持单精度浮点运算,从 Pentium III 开始就一直可用。双精度浮点运算需要 SSE2,并随 Pentium 4 引入。SSE2 始终 在 x86-64 CPU 上受支持。使用 ANDPS 还是 ANDPD 是由编译器的优化器决定的;有时您会看到 ANDPS 用于双精度浮点值,因为它只需要以正确的方式编写掩码。
此外,在支持 AVX 指令的 CPU 上,您通常会在 ANDPS/ANDPD 指令上看到一个 VEX 前缀,因此它变成 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