为什么编译器不将浮点 *2 优化为指数增量?

Why doesn't a compiler optimize floating-point *2 into an exponent increment?

本文关键字:优化 指数 编译器 为什么      更新时间:2023-10-16

我经常注意到gcc在可执行文件中将乘法转换为移位。当将intfloat相乘时,可能会发生类似的情况。例如,2 * f可以简单地将f的指数增加1,从而节省一些周期。如果有人要求编译器这样做(例如通过-ffast-math),编译器通常会这样做吗?

编译器通常足够聪明吗?还是我需要使用scalb*()ldexp()/frexp()函数族自己完成这项工作?

例如,2*f可以简单地将f的指数递增1,节省了一些周期。

这根本不是真的。

首先,有太多的角情况,如零、无穷大、楠和非标准化。那么你就有性能问题了。

误解是递增指数并不比相乘快

如果你看一下硬件指令,没有直接的方法来增加指数。所以你需要做的是:

  1. 逐位转换为整数
  2. 递增指数
  3. 逐位转换回浮点

在整数和浮点执行单元之间移动数据通常会有中到大的延迟。因此,最终,这种"优化"会变得比简单的浮点乘法差得多。

所以编译器之所以不进行这种"优化",是因为它没有更快。

在现代CPU上,乘法通常具有每周期一次的吞吐量和低延迟。如果该值已经在浮点寄存器中,那么通过对其进行整数运算是无法战胜它的。如果它一开始就在内存中,并且假设当前值和正确结果都不是零、非正规、nan或无穷大,那么执行之类的操作可能会更快

addl $0x100000, 4(%eax)   # x86 asm example

乘二;我唯一认为这是有益的一次是,如果你在一个远离零和无穷大的浮点数据数组上操作,而按2的幂缩放是你将要执行的唯一操作(所以你没有任何现有的理由将数据加载到浮点寄存器中)。

常见的浮点格式,特别是IEEE 754,不将指数存储为简单整数,将其视为整数不会产生正确的结果。

在32位浮点或64位双精度中,指数字段分别为8位或11位。指数代码1到254(以浮点形式)或1到2046(以双精度形式)的作用确实像整数:如果将其中一个值加1,结果是其中一个,则表示的值将加倍。然而,在这些情况下添加一个失败:

  • 初始值为0或低于正常值。在这种情况下,指数字段从零开始,加上一将使数字增加2-126(浮点)或2-1022;它不会使数字翻倍
  • 初始值超过2127(浮点)或2。在这种情况下,指数字段从254或2046开始,加一将数字更改为NaN;它不会使数字翻倍
  • 初始值为无穷大或NaN。在这种情况下,指数字段从255或2047开始,加1会将其变为零(并且可能溢出到符号位)。结果为零或次正规,但应分别为无穷大或NaN

(以上是正号。情况与负号对称。)

正如其他人所指出的,一些处理器不具备快速操作浮点值位的功能。即使在这样做的情况下,指数字段也不会与其他位隔离,因此在上面最后一种情况下,通常无法在不溢出到符号位的情况下向其中添加一个。

尽管一些应用程序可以容忍诸如忽略子规范或NaN甚至无穷大之类的快捷方式,但很少有应用程序可以忽略零。由于在指数上加一无法正确处理零,因此它不可用。

这与编译器或编译器编写器不聪明无关。这更像是遵守标准并产生所有必要的"副作用",如Infs、Nans和denormals。

此外,它可能是关于而不是产生其他不需要的副作用,例如读取记忆。但我确实认识到,在某些情况下,它可能会更快。

实际上,这就是硬件中发生的情况。

2也作为浮点数传递到FPU,尾数为1.0,指数为2^1。对于乘法,将指数相加,并将尾数相乘。

考虑到有专用硬件来处理复杂情况(乘以非二次方的值),并且特殊情况的处理不会比使用专用硬件更糟,因此没有必要使用额外的电路和指令。

下面是我看到的GCC 10:的实际编译器优化

x = 2.0 * hi * lo;

生成以下代码:

mulsd   %xmm1, %xmm0      # x = hi * lo;
addsd   %xmm0, %xmm0      # x += x;

如果您认为乘以2意味着指数增加1,请三思。以下是IEEE 754浮点运算的可能情况:

情况1:无穷大和NaN保持不变。

情况2:通过增加指数并将尾数(符号位除外)设置为零,将指数可能最大的浮点数更改为无穷大。

情况3:指数小于最大可能指数的标准化浮点数的指数增加一。Yippee!!!

情况4:尾数位设置最高的非标准化浮点数的指数增加一,将其转换为标准化数字。

情况5:尾数最高位被清除的非标准化浮点数,包括+0和-0,其尾数向左移动一位位置,使指数保持不变。

我非常怀疑,一个能够正确处理所有这些情况的生成整数代码的编译器是否会像处理器中内置的浮点一样快。它只适用于乘以2.0。对于4.0或0.5的乘积,适用一套全新的规则。对于乘以2.0的情况,您可能会尝试用x+x替换x*2.0,许多编译器都会这样做。也就是说,他们这样做,因为处理器可能能够同时进行一次加法和一次乘法,但不能每种都进行一次。因此有时您更喜欢x*2.0,有时更喜欢x+x,这取决于同时需要执行哪些其他操作。

对于嵌入式系统编译器来说,具有两次幂伪运算的特殊规模可能很有用,代码生成器可以以任何对机器最合适的方式对其进行转换,因为在一些嵌入式处理器上,关注指数可能比进行两次幂乘法快一个数量级,但在乘法最慢的嵌入式micros上,编译器可能会通过让浮点乘法例程在运行时检查其参数,从而跳过尾数中为零的部分,来实现更大的性能提升。

上一个关于2次幂乘法的Stackoverflow问题。共识和实际实现证明,不幸的是,目前没有比标准乘法更有效的方法了。