`strtof()` 转换错误超过 0.5 ULP

chu*_*ica 7 c floating-point

为什么strtof()“3.40282356779733 650000 e38”意外地转换为无穷大,即使它在 0.5 ULP 范围内FLT_MAX


FLT_MAX( float32 ) 是 0x1.fffffep+127 或大约 3.4028234663852885981170e+38。

上面的1/2 ULPFLT_MAX是 0x1.ffffffp+127 或大约 3.40282356779733 66163754 e+38,所以我期望低于此的任何十进制文本以及较低的文本在处于“舍入到最近”模式时FLT_MAX转换为。FLT_MAX

这是因为十进制文本从3.4028235677973388 642700FLT_MAX e38 增加到大约 3.4028235677973388 642700 e38 ,但对于高于该值的十进制文本值(如“3.40282356779733 650000 e38”),转换结果为infinity

下面是揭示问题的代码。它轻轻地爬升十进制文本字符串,寻找转换变为无穷大的值。
您的结果可能会有所不同,因为并非所有 C 实现都使用相同的浮点。

#include <assert.h>
#include <float.h>
#include <stdio.h>
#include <stdlib.h>

void bar(unsigned n) {
  char buf[100];
  assert (n < 90);
  int len = sprintf(buf, "%.*fe%d", n+1, 0.0, FLT_MAX_10_EXP);
  puts(buf);
  printf("%-*s   %-*s       %s\n", len, "string", n+3, "float", "double");
  float g = 0;
  for (unsigned i = 0; i < n; i++) {
    for (int digit = '1'; digit <= '9'; digit++) {
      unsigned offset = i ? 1+i : i;
      buf[offset]++;
      errno = 0;
      float f = strtof(buf, 0);
      if (errno) {
        buf[offset]--;
        break;
      }
      g = f;
    }
    printf("\"%s\" %.*e %a\n", buf, n + 3, g, atof(buf));
  }
  double delta = FLT_MAX - nextafterf(FLT_MAX, 0);
  double flt_max_ulp_d2 = FLT_MAX + delta/2.0;
  printf(" %.*e %a FLT_MAX + 1/2 ULP - 1 dULP\n", n + 3, nextafter(flt_max_ulp_d2,0),nextafter(flt_max_ulp_d2,0));
  printf(" %.*e %a FLT_MAX + 1/2 ULP\n", n + 3, flt_max_ulp_d2,flt_max_ulp_d2);
  printf(" %.*e %a FLT_MAX\n", n + 3, FLT_MAX, FLT_MAX);
  printf(" 1 23456789 123456789 123456789\n");
  printf("FLT_ROUNDS %d  (0: toward zero, 1: to nearest)\n", FLT_ROUNDS);
}

int main() {
  printf("%a %.20e\n", FLT_MAX, FLT_MAX);
  printf("%a\n", strtof("3.40282356779733650000e38", 0));
  printf("%a\n", strtod("3.40282356779733650000e38", 0));
  printf("%a\n", strtod("3.4028235677973366163754e+3", 0));
  bar(19);
}
Run Code Online (Sandbox Code Playgroud)

输出

0x1.fffffep+127 3.40282346638528859812e+38
inf
0x1.ffffffp+127
0x1.a95a5aaada733p+11
0.00000000000000000000e38
string                      float                        double
"3.00000000000000000000e38" 3.0000000054977557577780e+38 0x1.c363cbf21f28ap+127
"3.40000000000000000000e38" 3.3999999521443642490773e+38 0x1.ff933c78cdfadp+127
"3.40000000000000000000e38" 3.3999999521443642490773e+38 0x1.ff933c78cdfadp+127
"3.40200000000000000000e38" 3.4020000005553803402978e+38 0x1.ffe045fe9918p+127
"3.40280000000000000000e38" 3.4027999387901483621794e+38 0x1.ffff169a83f08p+127
"3.40282000000000000000e38" 3.4028200183756559773331e+38 0x1.ffffdbd19d02cp+127
"3.40282300000000000000e38" 3.4028230607370965250836e+38 0x1.fffff966ad924p+127
"3.40282350000000000000e38" 3.4028234663852885981170e+38 0x1.fffffe54daff8p+127
"3.40282356000000000000e38" 3.4028234663852885981170e+38 0x1.fffffeec5116ep+127
"3.40282356700000000000e38" 3.4028234663852885981170e+38 0x1.fffffefdfcbbcp+127
"3.40282356770000000000e38" 3.4028234663852885981170e+38 0x1.fffffeffc119p+127
"3.40282356779000000000e38" 3.4028234663852885981170e+38 0x1.fffffefffb424p+127
"3.40282356779700000000e38" 3.4028234663852885981170e+38 0x1.fffffeffffc85p+127
"3.40282356779730000000e38" 3.4028234663852885981170e+38 0x1.fffffefffff9fp+127
"3.40282356779733000000e38" 3.4028234663852885981170e+38 0x1.fffffefffffeep+127
"3.40282356779733600000e38" 3.4028234663852885981170e+38 0x1.fffffeffffffep+127

"3.40282356779733640000e38" 3.4028234663852885981170e+38 0x1.fffffefffffffp+127 <-- Actual
"3.40282356779733660000e38" 3.4028234663852885981170e+38 ...                    <-- Expected

"3.40282356779733642000e38" 3.4028234663852885981170e+38 0x1.fffffefffffffp+127
"3.40282356779733642700e38" 3.4028234663852885981170e+38 0x1.fffffefffffffp+127
 3.4028235677973362385861e+38 0x1.fffffefffffffp+127 FLT_MAX + 1/2 ULP - 1 dULP
 3.4028235677973366163754e+38 0x1.ffffffp+127 FLT_MAX + 1/2 ULP
 3.4028234663852885981170e+38 0x1.fffffep+127 FLT_MAX
 1 23456789 123456789 123456789
FLT_ROUNDS 1  (0: toward zero, 1: to nearest)
Run Code Online (Sandbox Code Playgroud)

注:GNU C11 (GCC) 版本 11.3.0 (x86_64-pc-cygwin) 由 GNU C 版本 11.3.0、GMP 版本 6.2.1、MPFR 版本 4.1.0、MPC 版本 1.2.1、isl 版本 isl-0.25 编译-GMP

[编辑] 的确切值FLT_MAX + 1/2 ULP: 0x1.ffffffp+127 340282356779733 661637539395458142568448 .0

今天,当我尝试确定传递给返回有限值的最大十进制文本 strtof()float时,我偶然发现了这个问题。

chu*_*ica 4

这是一个我可以回答我自己的问题吗?回答。欢迎其他答案。

为什么strtof()“3.40282356779733 650000 e38”意外地转换为无穷大,即使它在 0.5 ULP 范围内FLT_MAX

当然是双舍入
这里的“双”是指做某件事两次,而不是类型double

令上面ULP的 1/2为 0x1.ffffffp+127 或大约 3.40282356779733 66163754 e+38 称为阈值float FLT_MAX

大约 3.40282356733 64274808double e38 是低于阈值的 ULP的一半。显然,像“3.40282356779733 650000 e38”这样的值会过早舍入阈值Threshold,作为 a ,是和下一个较大值之间的一半(如果编码被扩展)。作为半平局,它四舍五入到“偶数”值 - 在本例中是较大的值。由于下一个较大值超出了最大可编码有限值,因此结果为无穷大doublefloatFLT_MAXfloatfloat

结论

  • 更好的方法strtof()可以正确处理这个极端情况。

  • FLT_DECIMAL_DIG + 3 相反,将过去的小数位(见下文)视为噪音是合理的strtof()


在替代strtof()实现中,IEEE_754允许这种十进制文本转换,以将传递一定意义的所有十进制数字视为零。这样,float当接近 2 秒的 1/2 路点时,允许转换到第二个最接近的值float。对于 common float,其重要性为FLT_DECIMAL_DIG + 3或 12 位小数。这里没有使用它,因为第 19 位的小数会影响结果。