首页 > 解决方案 > 使用 C11 和 GNU11 编译器标志的机器 epsilon 计算不同

问题描述

在使用 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,在 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标志的原因。我一直在谷歌上搜索并发现:https ://gcc.gnu.org/onlinedocs/gcc/C-Extensions.html但我仍然无法解释行为上的差异。

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

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

标签: cgccgnuc11epsilon

解决方案


在 C 中,获取floator doubleepsilon 的最佳方法是包含<float.h>并使用FLT_MINor DBL_MIN

C 标准并未完全指定的值,7.0/3.0 - 4.0/3.0 - 1.0;因为它允许实现以比标称类型更精确的精度评估浮点表达式。在某种程度上,这可以通过使用强制转换或赋值来处理。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;

请注意,需要一个静态断言来确保浮点基数符合预期,以便该 kludge 运行。代码还应该包含解释这个坏主意的文档:

  • 分数 ⅓ 的二进制表示以“01010101…”的无限序列结束。
  • 当 4/3 或 7/3 的二进制四舍五入到固定精度时,就好像数字被截断并向下或向上舍入,这取决于截断后的下一个二进制数字是 0 还是 1。
  • 鉴于我们假设浮点使用以二为底的基数,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 的低位位置的值,除了它可能是正数或负数。

推荐阅读