【问题标题】:Why doesn't a compiler optimize floating-point *2 into an exponent increment?为什么编译器不将浮点 *2 优化为指数增量?
【发布时间】:2012-10-06 19:37:26
【问题描述】:

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

编译器通常足够聪明来执行此操作,还是我需要使用scalb*()ldexp()/frexp() 函数族自己执行此操作?

【问题讨论】:

  • 你的问题是什么?
  • 问题是为什么编译器不将浮点乘以 2 转换为指数增量,就像整数和移位一样。
  • 我知道它可以,但是一般编译器会这样做吗?他们已经足够聪明了吗?还是我需要自己做?
  • @user1095108: 我做了一个 ldexp/increment/freexp 的基准测试。至少在我的测试中(VC++ 和 gcc,在 x86 上),乘法快得多

标签: c++ c performance optimization compiler-optimization


【解决方案1】:

例如,2 * f,可能只是将 f 的指数增加 1, 节省一些周期。

这根本不是真的。

首先,您有太多的极端情况,例如零、无穷大、南和非正规。那么你就有了性能问题。

误解是增加指数并不比做乘法快。

如果您查看硬件说明,则没有直接增加指数的方法。 所以你需要做的是:

  1. 按位转换为整数。
  2. 增加指数。
  3. 按位转换回浮点数。

在整数和浮点执行单元之间移动数据通常存在中等到较大的延迟。所以最后,这种“优化”变得比简单的浮点乘法差很多。

所以编译器不做这个“优化”的原因是它没有更快。

【讨论】:

  • 我假设 x86,因为您没有指定。但对于其他架构来说并没有太大的不同。
  • 是的,需要转换。在 C/C++ 中,您可以通过联合使用类型双关来执行此操作。由于现代处理器的设计方式,这种“转换”并不便宜。 (尤其是使用单独的整数和浮点单位。)
  • @user1095108 它可能拥有硬件,但不能通过指令(单独)访问它。将其视为类的私有函数。
  • @AkiSuihkonen 是的,这就是为什么我从该 3 步过程中省略了掩蔽/移动步骤。昂贵的部分是按位转换本身(类型双关语)。
  • 内置与否,你真的看过它们生成的指令吗?您甚至对它们进行了基准测试吗?不管编译器做什么,它仍然必须尊重 ISA。
【解决方案2】:

在现代CPUs 上,乘法通常具有每周期一个吞吐量和低延迟。如果该值已经在浮点寄存器中,那么您将无法通过杂耍它来对表示进行整数运算来击败它。如果它一开始就在内存中,并且如果您假设当前值和正确结果都不是零、非正规、南或无穷大,那么执行类似 可能会更快/p>

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

乘以二;我唯一能看到这是有益的情况是,如果您正在对远离零和无穷大的整个浮点数据数组进行操作,并且按 2 的幂进行缩放是您将要执行的唯一操作(所以您没有任何现有理由将数据加载到浮点寄存器中)。

【讨论】:

  • +1,在这种情况下,您可以将所有这些比例因子相加,最后只做一次。
  • +1 表示它实际上可能更快(尽管我怀疑它是否值得这样做)
  • 如果您在低端/嵌入式系统上使用多通道、高采样率的浮点音频,并且大多只是通过它,这种优化可能很有用.
【解决方案3】:

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

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

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

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

正如其他人所指出的,一些处理器没有快速处理浮点值位的设施。即使在那些这样做的情况下,指数字段也不会与其他位隔离,因此在上述最后一种情况下,您通常不能在不溢出到符号位的情况下向其添加一个。

虽然某些应用程序可以容忍快捷方式,例如忽略次正规数或 NaN 甚至无穷大,但应用程序很少能忽略零。由于指数加一无法正确处理零,因此无法使用。

【讨论】:

  • 如您所见,您可能提前知道假设浮动不为零,即使它很少见。
【解决方案4】:

这与编译器或编译器编写者不聪明无关。这更像是遵守标准并产生所有必要的“副作用”,例如 Infs、Nans 和非规范化。

它也可能是关于产生其他不需要的副作用,例如读取内存。但我确实认识到在某些情况下它可以更快。

【讨论】:

  • 也许在您的域中,我不关心 NaN。也不要忘记-ffast-math
  • @user1095108 但是编译器怎么知道你不在乎呢?
  • @user1095108 - 哇,你不关心 NaN 是什么意思?你关心 ISO 标准吗?
  • 可能有一些方法可以告诉编译器有多在乎...#pragmas 或属性。并且这些要求 OTOH 在某些领域已经放宽了,例如 opengl 着色器语言。
【解决方案5】:

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

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

鉴于有专用硬件来处理复杂情况(乘以不是 2 的幂的值),并且处理特殊情况并没有比使用专用硬件更糟糕,因此没有必要再添加电路和指令。

【讨论】:

    【解决方案6】:

    这是我在 GCC 10 中看到的实际编译器优化:

    x = 2.0 * hi * lo;
    

    生成此代码:

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

    【讨论】:

      【解决方案7】:

      对于嵌入式系统编译器来说,具有特殊的 2 的幂次方伪操作可能很有用,它可以由代码生成器以最适合所讨论机器的任何方式进行转换,因为在某些嵌入式处理器上关注指数可能比进行完全的二次幂乘法快一个数量级,但在乘法最慢的嵌入式微控制器上,编译器可能会通过浮点乘法例程获得更大的性能提升在运行时检查它的参数,以便跳过尾数为零的部分。

      【讨论】:

        【解决方案8】:

        A previous Stackoverflow question about multiplication by powers of 2。共识和实际实现证明,不幸的是,目前没有比标准乘法更有效的方法。

        【讨论】:

        • 您所说的对于当前的 x86 架构可能是正确的,但对于其他架构?
        • 嗯,唯一测试的其他架构是 x87,这是一场灾难
        • @user1095108:最佳方法可能是让编译器发出一个中间形式的指令,可以识别为“加/减/乘/除以常数”;如果编译的代码是针对没有 FPU 的处理器的,那么在许多情况下,使用常量的操作可以比普通的浮点乘法更有效地实现;事实上,在编译器支持“x87 风格”数学的日子里,这甚至比今天更真实 [extended-double 和 extended-float 的一个未被充分认识的特性是......
        • ...在许多情况下,没有 FPU 的机器可以比floatdouble 对扩展精度类型执行一系列操作更快。纯粹在float 中评估像16000003.0f-16000002.0f+16000000.0f 这样的表达式需要将中间结果放入移位和测试循环的多次迭代以对其进行规范化,即使它必须被移回以准备下一次添加。将 32 位尾数 不带隐含的 '1' 存储在与指数分开的寄存器中将避免此类困难。
        【解决方案9】:

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

        案例 1:Infinity 和 NaN 保持不变。

        案例 2:通过增加指数并将除符号位以外的尾数设置为零,将具有最大可能指数的浮点数更改为无穷大。

        案例 3:指数小于最大可能指数的归一化浮点数的指数增加一。哎呀!!!

        案例 4:设置了最高尾数位的非规范化浮点数的指数增加了 1,将它们变成了规范化数字。

        案例 5:清除了最高尾数位的非规格化浮点数,包括 +0 和 -0,其尾数向左移动一位,而指数保持不变。

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

        【讨论】:

          猜你喜欢
          • 2020-09-19
          • 2018-03-22
          • 1970-01-01
          • 1970-01-01
          • 2011-05-06
          • 2012-08-10
          • 2019-07-02
          • 2015-09-15
          • 1970-01-01
          相关资源
          最近更新 更多