【问题标题】:Can floating point multiplication by zero be optimised at runtime?可以在运行时优化浮点乘以零吗?
【发布时间】:2013-02-19 07:34:00
【问题描述】:

我正在编写一个算法来找到一个 nxn 矩阵的逆矩阵。让我们以 3x3 矩阵的具体情况为例。

当您手动反转矩阵时,您通常会查找包含一个或多个零的行/列,以加快行列式计算,因为它消除了您需要计算的项。

按照 C/C++ 中的这个逻辑,如果你用一个或多个零标识一行/列,你最终会得到以下代码:

float term1 = currentElement * DetOf2x2(...);
//           ^
//           This is equal to 0.
//
// float term2 = ... and so on.

由于编译器无法知道currentElement 在编译时为零,因此无法将其优化为float term = 0; 之类的东西,因此浮点乘法将在运行时执行。

我的问题是,这些零值是否会使浮点乘法更快,或者无论currentElement 的值如何,乘法都会花费相同的时间?如果在运行时无法优化乘法,那么我可以删除搜索包含零的行/列的逻辑。

【问题讨论】:

  • 取决于 FPU 设计。有些会更快,有些不会。
  • 为什么不做个测试看看?

标签: c++ c optimization floating-point multiplication


【解决方案1】:

现代 CPU 实际上会非常快速地处理乘以零,比一般的乘法更快,并且比分支更快 很多。除非该零将通过至少几十条指令传播,否则甚至不必费心尝试对其进行优化。

【讨论】:

  • 但是零将通过相当多的指令传播,并且正确预测的分支在零周期内执行,而在一般情况下,乘法可能会以一比二执行.
  • 关于定性的'much',假设DetOf2x2只包含正常的1/(adbc),多浮点指令是否仍然比a快?分支?或者是系统实现特定的?
  • 目前,我很难找到 FP 乘以零的硬件优化源 - 这可能仅适用于标量整数指令。无论如何,几乎可以肯定,2x2 行列式的计算太小,不值得跳过一个分支。例如,英特尔的 Sandy Bridge 可以在同一个时钟周期内完成独立的 FP 向量乘法、加法和混洗操作,因此您可能可以将 3 个 2x2 行列式的计算流水线化为只需要十几个周期的指令序列。我建议将 3x3 行列式实现为单个函数。
【解决方案2】:

除非计算很简单(例如所有常量),否则不允许编译器对此进行优化。

原因是,DetOf2x2 可能返回一个 NAN 浮点值。将 NAN 与零相乘不会返回零,而是再次返回 NAN。

您可以在这里使用这个小测试自己尝试一下:

int main (int argc, char **args)
{
  // generate a NAN
  float a = sqrt (-1);

  // Multiply NAN with zero..
  float b = 0*a;

  // this should *not* output zero
  printf ("%f\n", b);
}

如果你想优化你的代码,你必须自己测试为零。编译器不会为你这样做。

【讨论】:

  • 我想用 -O4 -ffast-math 编译器可以忽略 NaN。
【解决方案3】:

在运行时执行的优化称为 JIT(即时)优化。在翻译(编译)时执行的优化称为 AOT(提前)优化。您指的是 JIT 优化。编译器可能会在您的机器代码中引入 JIT 优化,但与常见的 AOT 优化相比,它的实现肯定要复杂得多。优化通常是根据重要性实现的,这种“优化”可能会被视为对其他算法产生负面影响。不需要 C 实现来执行任何这些优化。

您可以手动提供优化,这将是“搜索包含零的行/列的逻辑”,或者类似这样:float term1 = currentElement != 0 ? currentElement * DetOf2x2(...) : 0;

【讨论】:

  • 还有硬件优化。他似乎在询问ALU级别。可以肯定地说,除非在模拟器上运行,否则 C++ 实现不会执行 JIT 优化。
  • @Potatoswatter 英特尔的 C++ 编译器肯定会执行 JIT 优化。
  • 你有这方面的参考吗?谷歌什么也没告诉我,这样的功能应该会成为新闻。 Profile-guided optimization (PGO) 是 AOT 和 JIT 之间的另一类,主要供应商都实现了它;你是这个意思吗?
  • 不,这只是常规的 AOT 优化,使用不同的机器描述执行了多次。结果将是(在 Apple 术语中)“胖二进制文件”,因为它包含至少部分程序的多个副本。
【解决方案4】:
float term1 = currentElement * DetOf2x2(...);

即使 currentElement 为 0,编译器也会调用 DetOf2x2(...):这肯定比最终乘法的成本要高得多,无论是否乘以 0。原因有很多:

  • DetOf2x2(...) 可能会产生副作用(例如输出到日志文件),即使 currentElement 是 0,也会发生这种情况,并且
  • DetOf2x2(...) 可能会返回诸如 Not-a-Number / NaN sentinel 之类的值,无论如何都应该传播到 term1(正如 Nils Pipenbrinck 首先指出的那样)

鉴于DetOf2x2(...) 几乎肯定正在处理只能在运行时确定的值,因此不能排除后一种可能性在编译时。

如果您想避免调用Detof2x2(...),请尝试:

float term1 = (currentElement != 0) ? currentElement * DetOf2x2(...) : 0;

【讨论】:

  • 我怀疑编译器会在此处生成条件移动。
  • 这是正确的 - 寻找零的目的不是为了避免乘法,而是为了避免DetOf2x2() 计算。
  • 关于 Potatoswatter 对 user57368 的回答的评论,鉴于DetOf2x2 仅包含典型值,?: 运算符生成的 asm 分支是否会比简单地相乘更快:1 / (a * d - b * c) ?
  • @PLPiper:特别是如果 DetOf2x2 是内联的,这将是一个近距离的电话,答案可能取决于您的特定硬件:必须进行分析才能找出答案。 (顺便说一句 - 我假设您在尝试除法之前知道 ad != bc ......)
  • 好的,谢谢! DetOf2x2 将被内联,因此我将进行分析。 (是的,关于 1/0 问题!)
【解决方案5】:

当编译器可以猜测“currentElement”的值时,以下构造在编译时有效。

float term1 = currentElement ? currentElement * DetOf2x2(...) : 0;

如果在编译时无法猜测,则会在运行时进行检查,性能取决于处理器架构:分支之间的权衡(包括分支延迟和重建指令管道的延迟)到 10 或 20 个周期)和平面代码(某些处理器每个周期运行 3 条指令)和硬件分支预测(当硬件支持分支预测时)。

由于 x86_64 处理器上的乘法吞吐量接近 1 个周期,因此不存在取决于 0.0、1.0、2.0 或 12345678.99 等操作数值的性能差异。如果存在这样的差异,那将被视为加密风格软件中的隐蔽通道。

GCC 允许在编译时检查函数参数

inline float myFn(float currentElement, myMatrix M)

{

#if __builtin_constant_p(currentElement) && currentElement == 0.0

返回 0.0;

#else

return currentElement * det(M);

#endif

}

您需要在编译器中启用内联和过程间优化。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2023-04-01
    • 2011-12-17
    • 2018-11-26
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-08-13
    相关资源
    最近更新 更多