使用 AVX2 和 /fp:fast 的 Intel 2022 编译器出现 Nan 问题

问题描述 投票:0回答:1

我正在使用 @njaffa 的另一篇文章中描述的测试工具,试图制作一个更快、更准确的 erfc() 函数,这时我遇到了一个我仍然无法完全理解的特殊错误,并且需要一些帮助。我认为代码在突破某些界限的同时表现得非常好。

纯粹主义者可能会反对循环终止时计数器 argi 溢出,但这不是问题。当使用通常的 go fast 选项(特别是 /fp:fast)编译时,生成的代码是错误的。我添加了一些不影响问题的诊断并计算检测到的 nan 数量(但给出不一致的结果)。

NB 它仅在英特尔编译器上失败,并且必须设置为 /fp:fast 并发布(所有其他选项都可以正常工作,就像我尝试过的所有其他编译器一样)。 FP 严格和精确都可以在 Intel 上正常工作。

我尽可能地简化了他的代码,同时仍然保留了我在这里看到的错误行为,并且在下面以失败状态复制了它:

#include <stdlib.h>
#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include <math.h>

// njuffa's fast erfc(x) and test harness (stripped down as far as I can) see 
// https://stackoverflow.com/questions/77741402/fast-implementation-of-complementary-error-function-with-5-digit-accuracy/

// It only fails on the latest Intel 2022 compiler when /fp:fast and /O2 /Ob2 /Ot

/* Fast computation of the complementary error function. For argument x > 0
erfc(x) = exp(-x*x) * P(x) / Q(x), where P(x) and Q(x) are polynomials. 
If expf() is faithfully rounded, the following error bounds should hold:
Maximum relative error < 1.065e-5, maximum absolute error < 9.50e-6, and 
maximum ulp error < 176.5
*/
float fast_erfcf (float x)
{
  float a, c, e, p, q, r, s;
  a = fabsf (x);
  c = fminf (a, 10.5f);
  s = -c * c;
  e = expf (s);
  q =         3.82346272e-1f; //  0x1.8785c8p-2f
  p =        -4.38243151e-5f; // -0x1.6fa000p-15
  q = q * c + 1.30382371e+0f; //  0x1.4dc764p+0
  p = p * c + 2.16852218e-1f; //  0x1.bc1d04p-3
  q = q * c + 1.85278797e+0f; //  0x1.da5050p+0
  p = p * c + 7.23953605e-1f; //  0x1.72aa0cp-1
  q = q * c + 9.99991596e-1f; //  0x1.fffee6p-1
  p = p * c + 1.00000000e+0f; //  0x1.000000p+0
  r = e / q;
  r = r * p;
  if (x < 0.0f) r = 2.0f - r;
  if (isnan(x)) r = x + x;
  return r;
}

float uint32_as_float (uint32_t a)
{
  float r;
  memcpy (&r, &a, sizeof r);
  return r;
}

uint32_t float_as_uint32 (float a)
{
  uint32_t r;
  memcpy (&r, &a, sizeof r);
  return r;
}

uint64_t double_as_uint64 (double a)
{
  uint64_t r;
  memcpy (&r, &a, sizeof r);
  return r;
}

double floatUlpErr (float res, double ref)
{
  uint64_t refi, i, j, err;
  int expoRef;

  /* ulp error cannot be computed if either operand is NaN, infinity, zero */
  if (isnan (res) || isnan (ref) || isinf(res) || isinf (ref) ||
     (res == 0.0f) || (ref == 0.0)) {
    return 0.0;
  }
  i = ((int64_t)float_as_uint32 (res)) << 32;
  expoRef = (int)(((double_as_uint64 (ref) >> 52) & 0x7ff) - 1023);
  refi = double_as_uint64 (ref);
  if (expoRef >= 129) {
      j = (refi & 0x8000000000000000ULL) | 0x7fffffffffffffffULL;
  } else if (expoRef < -126) {
      j = ((refi << 11) | 0x8000000000000000ULL) >> 8;
      j = j >> (-(expoRef + 126));
      j = j | (refi & 0x8000000000000000ULL);
  } else {
      j = ((refi << 11) & 0x7fffffffffffffffULL) >> 8;
      j = j | ((uint64_t)(expoRef + 127) << 55);
      j = j | (refi & 0x8000000000000000ULL);
  }
  err = (i < j) ? (j - i) : (i - j);
  return err / 4294967296.0;
}

int main (void)
{
  uint32_t argi, last_argi;
  float arg, res, reff, abserrloc = NAN, ulperrloc = NAN;
  double ref, abserr, ulperr;
  double maxabserr = -0x1.2468Ap-127, maxulperr = -0x1.13579p-127; // tagged to spot in MMXreg
  unsigned int nancount = 0, loopcount = 0;

  //    argi = 0; // original code to test entire range - uses XMM8h,XMM10h & fails
  argi = float_as_uint32(-4.0); // this is a convenient hard failing value aka 0xc0800000 - uses XMM10h, XMM13h & fails
  //    argi = 0xff810000-0x007f0000; // this is the closest start value I found that fails with increment 0x007f0000

  do {
     if (argi == 0xff810000)
        printf("This time it will fail\n");
     arg = uint32_as_float (argi);
     ref = erfc ((double)arg);
     res = fast_erfcf (arg);
     ulperr = floatUlpErr (res, ref);
     if (ulperr > maxulperr) {
         ulperrloc = arg;
         maxulperr = ulperr;
     }
     abserr = fabs (res - ref);
     if (isnan(abserr)) {
        nancount++; // testing for nan here the bug persists
        printf("NaN @ %8x  ", argi); // this line will trigger breakpoint here
     }
     if (abserr > maxabserr) {
 //       if (isnan(abserr)) printf("abserr is Nan/n"); // testing for nan here makes results correct
        abserrloc = arg;
        maxabserr = abserr;
 //            printf("abserr %g @ %g ", abserr, arg);  // this line also makes results correct 

     }

     last_argi = argi;
//      argi++; // original code with Nan (0xffffffff)
//      argi += 0x007f0000; // the "nan" reported depends on this increment!
     argi += 0x00400000;

                        // 0x007F0000 is the largest increment that is still a Nan (0x7fc100000)
                        // 0x00400000 is Nan (0xffc00000)
                        // 0x00200000 is Nan (0xffe00000)
                        // 0x00100000 is Nan (0xfff00000) etc. 
     loopcount++;
  } while (argi>last_argi);  (false); // changing this line to prevent looping generates correct code
                          // NB it is radically different to the looping code (that fails).

  printf ("max abs err = %.6e %.6a %x @ % 15.8e %.6a\n"
          "max ulp err = %.6e @ % 15.8e\n",
          maxabserr, maxabserr, float_as_uint32(maxabserr), abserrloc, abserrloc,
          maxulperr, ulperrloc);
  printf("Nan count %i / %i\n", nancount, loopcount);
  return EXIT_SUCCESS;
}

如果它按预期工作,那么输出应该是(给出或接受舍入误差):

NaN @ ffc00000
max abs err = 1.541726e-08 0x1.08ddd1p-26 32846ee9 @ -4.00000000e+00 -0x1.000000p+2
max ulp err = 1.293293e-01 @ -4.00000000e+00

使用 MS VC IDE 的命令行选项有:

 /GS /W3 /Gy /Zi /O2 /Ob2 /fp:fast /D "NDEBUG" /D "_CONSOLE" /D "_UNICODE" /D "UNICODE"
 /Qipo /Zc:forScope /arch:CORE-AVX2 /Oi /MD /Fa"x64\Release\" /EHsc /nologo 
 /Fo"x64\Release\" //fprofile-instr-use "x64\Release\" /Ot 
 /Fp"x64\Release\erfc_njaffa.pch" 

因为它可能相关,CPU 是 i5-12600K AlderLake Family 6 Model 7 Step 2 Ext Model 97 Rev C0,我在 Win 11 Pro 下运行。

症状是,当 optimization 和 fp:fast 一起使用时,由于一些奇怪的编译器怪癖,absmaxerr 最终会出现 nan。我可以在某种程度上跟踪反汇编代码的执行,并用独特的负范数标记 max____ 变量,以使它们在 XMMn 寄存器集中脱颖而出。但我一直无法弄清楚它是如何发生的。我认为这可能是因为用 nans 执行库代码(测试所有值)。

代码的微小更改会导致生成的代码(和/或寄存器分配)发生根本性变化。特别是,必须至少执行一次循环分支才能显示问题。如果最后使用 while(false) ,编译器会生成完全不同(且有效)的代码,同样,如果防御测试或 printf 被放置在关键位置。

abserr is nan 有两项测试 - 一项在外循环级别处于活动状态,触发 0x7f800000maxabserr 路径。如果在那里启用,它永远不会触发,并且代码可以正常工作,而外部测试继续看到 nan 的。

我的预感是,当 argi 是 nan 时,最大abs值更新逻辑和 maxabserr 的存储之间存在一些特殊的交互,但我无法确定它。 abserrloc 在第一个循环后不会改变并保持在 -4.0,但最初正确的 maxabserr 被践踏。感谢您的启发。

c floating-point nan avx2 icx
1个回答
0
投票

潜在问题:

>
与 NAN

C 没有指定

if (abserr > maxabserr)
会发生什么并且涉及 NAN。 IEEE 确实对此类比较有指定的功能,但 C 没有义务遵循它们。

为了更便携的功能而建议的更改:

 if (isnan(abserr)) {
    nancount++;
    printf("NaN @ %8x  ", argi);
 }
 // if (abserr > maxabserr) {
 else if (abserr > maxabserr) {
    abserrloc = arg;
    maxabserr = abserr;
 }
© www.soinside.com 2019 - 2024. All rights reserved.