IEEE-754 浮点运算、相等性和缩小

8
在下面的代码中,函数foo1、foo2和foo3旨在等效。但是在运行时,foo3在循环中不会终止,这是为什么呢?
template <typename T>
T foo1()
{
   T x = T(1);
   T y = T(0);
   for (;;)
   {
      if (x == y) break;
      y = x;
      ++x;
   }
   return x;
}

template <typename T>
T foo2()
{
   T x = T(0);
   for (;;)
   {
      T y = x + T(1);
      if (!(x != y)) break;
      ++x;
   }
   return x;
}

template <typename T>
T foo3()
{
   T x = T(0);
   while (x != (x + T(1))) ++x;
   return x;
}

int main()
{
   printf("1 float:  %20.5f\n", foo1<float>());
   printf("2 float:  %20.5f\n", foo2<float>());
   printf("3 float:  %20.5f\n", foo3<float>());
   return 0;
}

注意:这是使用VS2010在发布模式下编译的,使用/fp precise选项。不确定GCC等如何处理此代码,希望能提供任何信息。可能是foo3中x和x+1值变成NaN的问题吗?

3
有趣的问题。在gcc 4.2.1上,所有三个函数都按预期终止。我倾向于认为这是Visual Studio中的一个漏洞。 - ComicSansMS
3
听起来像是一个过于热衷于优化的问题(即编译器bug)。 - Mark Dickinson
2
再想一想,我撤回关于错误的说法。至少对于C语言(我不太了解C++),编译器在计算表达式时允许使用额外的精度(假设没有分配给本地变量或显式转换)。我认为(但需要检查)这适用于第3个示例中的x + T(1)。所以我的猜测是,和与比较都是使用double类型执行的,因此比较始终为真,但在x达到2 ** 24后就不再移动了。如果没有其他人到达,稍后将编写一个经过适当研究的答案。 - Mark Dickinson
2
此外,根据IEEE 754标准,如果值超出范围,则缩小转换将导致+-inf。尝试例如“float x = DBL_MAX;”。 - janneb
4
顺便提一下,C99中相关的部分(大概C++也有类似的)在第6.3.1.8节第2段中:“浮点操作数的值及其表达式结果的值可能比类型所需的精度和范围更加精确和广泛,但类型不会因此而改变。” 因此,尽管这种行为令人惊讶和烦人,但这并不是编译器的错误。 - Mark Dickinson
显示剩余9条评论
1个回答

13

很可能发生的情况是这样的,在x86架构上,中间计算可以使用80位精度(long double是相应的C/C++类型)。编译器在(+1)操作和(!=)操作中使用了全部80位,但在存储之前截断了结果。

所以你的编译器实际上做的是这个:

while ((long double)(x) != ((long double)(x) + (long double)(1))) {
  x = (float)((long double)(x) + (long double)(1));
} 

这绝对不符合IEEE标准,会给每个人带来无尽的头痛,但这是MSVC的默认设置。使用/fp:strict编译器标志来禁用此行为。

这是我大约十年前对这个问题的记忆,如果有不完全正确之处请见谅。请参考此处的官方Microsoft文档

编辑 我很惊讶地发现,默认情况下g++也表现出完全相同的行为(在i386 linux上,但不是例如-mfpmath=sse)。


4
仅仅使用 double 而不是 long double 就足以导致问题,但据我所知,即使在 64 位构建中,微软仍然喜欢使用 x87 FPU,因此似乎更有可能会使用 long double - Mark Dickinson
1
非常有见地的回答,但我注意到在VS2008上,即使使用/fp:strictfoo3()仍然无法终止? - acraig5075
2
@acraig5075:同意;我不明白/fp:strict如何在这里有所帮助。对于/fp:precise,微软的文档甚至指定“表达式计算将遵循C99 FLT_EVAL_METHOD=2”,证实了中间值计算为长双精度类型。对于/fp:strict,文档没有说明,但我不认为会出现FLT_EVAL_METHOD=0类似的行为。 - Mark Dickinson
@acraig5075:奇怪的是,在x86/x87 32位架构上,g++也会这样做。因此,这可能是常见的x87行为。 - n. m.

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接