我经常注意到 gcc 将乘法转换为可执行文件中的移位。int
将 a和 a相乘时可能会发生类似的情况float
。例如,2 * f
, 可能只是将 的指数增加f
1,从而节省一些周期。编译器,也许如果有人要求他们这样做(例如 via -ffast-math
),通常会这样做吗?
编译器通常足够聪明来做到这一点,还是我需要自己使用scalb*()
orldexp()/frexp()
函数系列来做到这一点?
我经常注意到 gcc 将乘法转换为可执行文件中的移位。int
将 a和 a相乘时可能会发生类似的情况float
。例如,2 * f
, 可能只是将 的指数增加f
1,从而节省一些周期。编译器,也许如果有人要求他们这样做(例如 via -ffast-math
),通常会这样做吗?
编译器通常足够聪明来做到这一点,还是我需要自己使用scalb*()
orldexp()/frexp()
函数系列来做到这一点?
例如,2 * f,可能只是将 f 的指数增加 1,从而节省一些周期。
这根本不是真的。
首先,您有太多的极端情况,例如零、无穷大、南和非正规。然后你有性能问题。
误解是增加指数并不比做乘法快。
如果您查看硬件说明,则没有直接增加指数的方法。所以你需要做的是:
在整数和浮点执行单元之间移动数据通常存在中等到较大的延迟。所以最后,这种“优化”变得比简单的浮点乘法差得多。
所以编译器不做这个“优化”的原因是它没有更快。
在现代CPU上,乘法通常具有每周期一个吞吐量和低延迟。如果该值已经在浮点寄存器中,那么您将无法通过杂耍它来对表示进行整数运算来击败它。如果它一开始就在内存中,并且假设当前值和正确结果都不是零、非正规、南或无穷大,那么执行类似的操作可能会更快
addl $0x100000, 4(%eax) # x86 asm example
乘以二;我唯一能看到这是有益的情况是,如果您正在对远离零和无穷大的整个浮点数据数组进行操作,并且按 2 的幂进行缩放是您将要执行的唯一操作(所以您没有任何现有理由将数据加载到浮点寄存器中)。
常见的浮点格式,尤其是 IEEE 754,不会将指数存储为简单整数,将其视为整数不会产生正确的结果。
在 32 位浮点或 64 位双精度中,指数字段分别为 8 位或 11 位。指数代码 1 到 254(浮点数)或 1 到 2046(双精度数)确实像整数:如果将其中一个值加到一个并且结果是这些值之一,则表示的值加倍。但是,在这些情况下添加一个会失败:
(以上为正号。情况与负号对称。)
正如其他人所指出的,一些处理器没有快速操作浮点值位的设施。即使在那些这样做的情况下,指数字段也不会与其他位隔离,因此在上述最后一种情况下,通常不能在不溢出到符号位的情况下向其添加一个。
尽管某些应用程序可以容忍诸如忽略次正规或 NaN 甚至无穷大的捷径,但很少有应用程序可以忽略零。由于向指数加一无法正确处理零,因此它不可用。
这与编译器或编译器编写者不聪明无关。这更像是遵守标准并产生所有必要的“副作用”,例如 Infs、Nans 和非规范化。
它也可能是关于不产生其他不需要的副作用,例如阅读记忆。但我确实认识到在某些情况下它可以更快。
实际上,这就是硬件中发生的事情。
也作为2
浮点数传入 FPU,尾数为 1.0,指数为 2^1。对于乘法,指数相加,尾数相乘。
鉴于有专用硬件来处理复杂情况(乘以不是 2 的幂的值),并且处理特殊情况并没有比使用专用硬件更糟糕,因此拥有额外的电路和指令是没有意义的.
这是我在 GCC 10 中看到的实际编译器优化:
x = 2.0 * hi * lo;
生成此代码:
mulsd %xmm1, %xmm0 # x = hi * lo;
addsd %xmm0, %xmm0 # x += x;
对于嵌入式系统编译器来说,具有特殊的二乘幂伪操作可能很有用,它可以由代码生成器以对所讨论机器最优化的任何方式进行翻译,因为在一些嵌入式处理器上,专注于指数可能比进行完全的二次幂乘法快一个数量级,但是在乘法最慢的嵌入式微控制器上,编译器可能会通过让浮点乘法例程检查其参数来实现更大的性能提升在运行时跳过尾数为零的部分。
一个关于乘以 2 的幂的先前 Stackoverflow 问题。共识和实际实现证明,不幸的是,目前没有比标准乘法更有效的方法。
如果您认为乘以 2 意味着指数增加 1,请再想一想。以下是 IEEE 754 浮点运算的可能情况:
案例 1:Infinity 和 NaN 保持不变。
情况 2:通过增加指数并将除符号位以外的尾数设置为零,将具有最大可能指数的浮点数更改为无穷大。
情况 3:指数小于最大可能指数的归一化浮点数的指数增加 1。伊皮!!!
情况 4:设置了最高尾数位的非规范化浮点数的指数增加了 1,将它们变成了规范化数字。
情况 5:清除了最高尾数位的非规格化浮点数,包括 +0 和 -0,其尾数向左移动一位,而指数保持不变。
我非常怀疑生成正确处理所有这些情况的整数代码的编译器是否会与处理器中内置的浮点一样快。它只适合乘以2.0。对于乘以 4.0 或 0.5,适用一套全新的规则。对于乘以 2.0 的情况,您可能会尝试将 x * 2.0 替换为 x + x,许多编译器都会这样做。那就是他们这样做,因为处理器可能能够例如同时进行一次加法和一次乘法运算,但不能每种都进行一次。因此,有时您更喜欢 x * 2.0,有时更喜欢 x + x,这取决于其他操作需要同时执行的操作。