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

Posted

技术标签:

【中文标题】fabs(double) 如何在 x86 上实现?这是一项昂贵的手术吗?【英文标题】:How would fabs(double) be implemented on x86? Is it an expensive operation? 【发布时间】:2017-06-19 11:59:58 【问题描述】:

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

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

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

【问题讨论】:

谢谢。好的,假设它是 C,平台是 x86 仅供参考:gcc 和 clang 都是开源的,您可以很容易地自己找到并查看源代码。 “(乘法和平方根的组合)?”——如果fabs() 甚至abs() 的任何实现尝试了这个,我会非常震惊。 认真的吗?这怎么太宽泛了?这似乎是一个非常合理的问题。 为了回答这个问题,在 x86 上,它通常由 ANDPD 指令实现(基本上是浮点数上的按位与)。这是一条相当快的指令,通常为 1 个时钟周期。 【参考方案1】:

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

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

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

(图片由 Codekaizen 提供,通过 Wikipedia,在 CC-bySA 下获得许可。)

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

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

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

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

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

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

如果您真的有兴趣深入研究,您可能会看到this answer(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 指令上看到一个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

【讨论】:

亲爱的@CodyGray,非常感谢您提供这个全面而易于理解的答案!它的公式非常好,听起来像是从教科书中复制的:) 不客气。我认为这个问题值得回答,尽管人们对其最初的表述感到担忧。这个话题可以变得非常复杂,但我尽量保持它尽可能简单,而不要over - 简化。我很高兴你发现它有帮助,@Alex。但是,不是从教科书中复制的;几分钟后就在我的键盘上敲了一下。 :-) 我什至还没有找到涵盖这些类型的教科书。 如果你想要一个过于复杂的版本,请参阅我的 SSE absolute-value answer 与 C 内在函数。我试图让编译器即时生成掩码而不是加载它,这可能很愚蠢(而且对于某些编译器来说很难做到)。我应该去简化这个答案,因为部分问题是在寻找 _mm_uninitialized_ps(); 而不是编译器实际支持的实际 _mm_undefined_ps(); 另外,这个问题为 fabs() 实现提出了一些新颖的想法:例如从 0 中减去,然后是 maxps,这会起作用,但关键路径要长得多。 @Peter 感谢您的指点。我很久以前就赞成你的回答,但后来忘记了。我在此处包含了一个链接,因此它不会在 cmets 中丢失。如果我有时间,我会尽量记住深入研究,看看我是否可以让 MSVC 在那里生成你想要的代码。在一次性情况下,即时生成掩码可能是更好的解决方案,因为它避免了从内存加载常量的缓存未命中,但您似乎在多个地方假设常量将被复制。不是;当您调用 fabs 时,编译器会发出一个全局常量。

以上是关于fabs(double) 如何在 x86 上实现?这是一项昂贵的手术吗?的主要内容,如果未能解决你的问题,请参考以下文章

如何在带有 Provider 的 ListView 中返回 Future<double>?

格式化double,printf中至少有一个小数

如何在半视图上实现滑动手势和在另一半视图上实现平移手势?

如何在 BaseAdapter 上实现 getFilter?

机试练习总结01:fabs和abs

为啥 abs() 和 fabs() 在 C 的两个不同头文件中定义