与-O3相比,gcc -Ofast的汇编代码中计算不精确的来源在哪里?

Har*_*ger 2 c assembly gcc x86-64 fast-math

以下 3 行使用"gcc -Ofast -march=skylake"给出不精确的结果:

int32_t  i = -5;
const double  sqr_N_min_1 = (double)i * i;
1. - ((double)i * i) / sqr_N_min_1
Run Code Online (Sandbox Code Playgroud)

显然,第三行中的sqr_N_min_1gets25.(-5 * -5) / 25应该变为 ,1.因此第三行的整体结果正好是0.。事实上,这对于编译器选项"gcc -O3 -march=skylake"是正确的。

但是使用“-Ofast”,最后一行产生-2.081668e-17而不是0.i除了-5(例如67)之外的其他非常小的正或负随机偏差0.。我的问题是:这种不精确的根源究竟在哪里?

为了调查这个,我用 C 写了一个小测试程序:

#include <stdint.h>      /* int32_t */
#include <stdio.h>
#define MAX_SIZE 10

double W[MAX_SIZE];

int main( int argc, char *argv[] )
{
  volatile int32_t n = 6; /* try 6 7 or argv[1][0]-'0' */
  double           *w = W;
  int32_t          i = 1 - n;
  const int32_t    end = n - 1;
  const double     sqr_N_min_1 = (double)i * i;

  /* Here is the crucial part. The loop avoids the compiler replacing it with constants: */
  do {
    *w++ = 1. - ((double)i * i) / sqr_N_min_1;
  } while ( (i+=2) <= end );

  /* Then, show the results (only the 1st and last output line matters): */
  w = W;
  i = 1 - n;
  do {
    fprintf( stderr, "%e\n", *w++ );
  } while ( (i+=2) <= end );

  return( 0 );
}
Run Code Online (Sandbox Code Playgroud)

Godbolt 向我展示了由"x86-64 gcc9.3"生成的程序集,选项为"-Ofast -march=skylake""-O3 -march=skylake"。请检查网站的五栏(1. 源代码,2. 带有“-Ofast”的汇编,3. 带有“-O3”的汇编,4. 第一个汇编的输出,5. 第二个汇编的输出):

带有五列的 Godbolt 站点

正如您所看到的,程序集的差异是显而易见的,但我无法弄清楚不精确的确切来源。那么,问题是,哪些汇编指令对此负责?

一个后续问题是:是否有可能通过重新编写 C 程序来避免“-Ofast -march=skylake”的这种不精确性?

Pet*_*des 6

评论和另一个答案指出了在您的案例中发生的特定转换,使用互惠和 FMA 而不是除法。

是否有可能通过重新编写 C 程序来避免“-Ofast -march=skylake”的这种不精确性?

一般不会。

-Ofast是(当前)的同义词-O3 -ffast-math
https://gcc.gnu.org/wiki/FloatingPointMath

的部分-ffast-math就是-funsafe-math-optimizations,它顾名思义,可以改变数值结果。(为了允许更多优化,例如将 FP 数学视为关联以允许使用 SIMD 自动矢量化数组的总和,和/或使用多个累加器展开,或者甚至只是重新排列一个表达式中的一系列操作以组合两个单独的常量。)

这正是您通过使用该选项所要求的那种速度超过准确度的优化。如果您不希望那样,请不要启用所有-ffast-math子选项,只启用-fno-math-errno/ 之类的安全选项-fno-trapping-math。(请参阅如何强制 GCC 假定浮点表达式为非负数?


没有办法制定您的来源来避免所有可能的问题。

可能您可以volatile到处使用tmp 变量来破坏语句之间的优化,但这会使您的代码比-O3使用默认-fno-fast-math. 即便如此,对库函数的调用(例如sinlog可能会解析为假设 args 是有限的版本,而不是 NaN 或无穷大,因为-ffinite-math-only.

-Ofast 的 GCC 问题?指出另一个效果:isnan()被优化为编译时0.