使用 C11 和 GNU11 编译器标志的机器 epsilon 计算不同

Machine epsilon calculation is different using C11 and GNU11 compiler flags

使用 Python & Julia 时,我可以使用一个巧妙的技巧来研究特定浮点表示的机器 epsilon。

例如,在 Julia 1.1.1 中:

julia> 7.0/3 - 4/3 - 1
2.220446049250313e-16 

julia> 7.0f0/3f0 - 4f0/3f0 - 1f0
-1.1920929f-7

我目前正在学习 C 并编写了这个程序来尝试实现同样的目标:

#include <stdio.h>

int main(void)
{
  float foo;
  double bar;

  foo = 7.0f/3.0f - 4.0f/3.0f - 1.0f;
  bar = 7.0/3.0 - 4.0/3.0 - 1.0;

  printf("\nM.E. for float: %e \n\n", foo);
  printf("M.E. for double: %e \n\n", bar);

  return 0;
}

奇怪的是,我得到的答案取决于我使用的是 C11 还是 GNU11 编译器标准。我的编译器是 GCC 5.3.0,运行 on Windows 7 并通过 MinGW 安装。

简而言之,当我编译时:gcc -std=gnu11 -pedantic begin.c 我得到:

M.E. for float: -1.192093e-007

M.E. for double: 2.220446e-016

如我所料,匹配 Python 和 Julia。但是当我编译时:gcc -std=c11 -pedantic begin.c 我得到:

M.E. for float: -1.084202e-019

M.E. for double: -1.084202e-019

这是出乎意料的。我认为这可能是 GNU 特定的功能,这就是我添加 -pedantic 标志的原因。我一直在 google 上搜索并发现了这个:https://gcc.gnu.org/onlinedocs/gcc/C-Extensions.html 但我仍然无法解释行为上的差异。

明确地说,我的问题是:为什么使用不同的标准结果会不同?

更新:同样的差异适用于 C99 和 GNU99 标准。

在 C 中,获得 floatdouble epsilon 的最佳方法是包含 <float.h> 并使用 FLT_MINDBL_MIN

7.0/3.0 - 4.0/3.0 - 1.0; 的值未完全由 C 标准指定,因为它允许实现以比标称类型更精确的方式评估浮点表达式。在某种程度上,这可以通过使用强制转换或赋值来解决。 C 标准要求转换或赋值以“丢弃”过高的精度。这通常不是一个合适的解决方案,因为初始超额精度和“丢弃”超额精度的操作都可以进行舍入。这种双舍入可能会产生与完全使用标称精度计算不同的结果。

对问题中的代码使用转换变通方法会产生:

_Static_assert(FLT_RADIX == 2, "Floating-point radix must be two.");
float FloatEpsilon = (float) ((float) (7.f/3) - (float) (4.f/3)) - 1;
double DoubleEpsilon = (double) ((double) (7./3) - (double) (4./3)) - 1;

请注意,需要一个静态断言来确保浮点基数与预期的一样,才能让这个杂乱无章地运行。该代码还应该包括解释这个坏主意的文档:

  • 分数 ⅓ 的二进制表示以“01010101…”的无限序列结束。
  • 当 4/3 或 7/3 的二进制四舍五入到固定精度时,就好像数字被截断并向下或向上舍入,这取决于截断后的下一个二进制数字是 0 还是1.
  • 鉴于我们假设浮点数使用基数为 2 的基数,4/3 和 7/3 在连续的二进制数中(4/3 在 [1, 2] 中),7/3 在 [2 , 4).因此,它们的截断点相差一个位置。
  • 因此,我们转换为二进制浮点数格式,4/3和7/3的区别在于后者比前者多1,并且它的尾数提前一位结束。检查可能的截断点发现,除了初始差值 1 外,有效数的不同之处在于 4/3 中低位位置的值,尽管不同方向可能不同。
  • 根据 Sterbenz 引理,7/3 减去 4/3 不存在浮点误差,所以结果恰好是 1 加上上述差值。
  • 减1得到那个差值,就是4/3的低位位置的值,除了可能是正数也可能是负数。