为什么使用英特尔C++编译器时,NaN - NaN的结果为0.0?

305

众所周知在算术中 NaN 会传播,但我找不到任何证明,因此我写了一个小测试:

#include <limits>
#include <cstdio>

int main(int argc, char* argv[]) {
    float qNaN = std::numeric_limits<float>::quiet_NaN();

    float neg = -qNaN;

    float sub1 = 6.0f - qNaN;
    float sub2 = qNaN - 6.0f;
    float sub3 = qNaN - qNaN;

    float add1 = 6.0f + qNaN;
    float add2 = qNaN + qNaN;

    float div1 = 6.0f / qNaN;
    float div2 = qNaN / 6.0f;
    float div3 = qNaN / qNaN;

    float mul1 = 6.0f * qNaN;
    float mul2 = qNaN * qNaN;

    printf(
        "neg: %f\nsub: %f %f %f\nadd: %f %f\ndiv: %f %f %f\nmul: %f %f\n",
        neg, sub1,sub2,sub3, add1,add2, div1,div2,div3, mul1,mul2
    );

    return 0;
}

这个例子 (在此实时运行) 的输出基本符合我的期望(负数有点奇怪,但也算是有道理的):

neg: -nan
sub: nan nan nan
add: nan nan
div: nan nan nan
mul: nan nan

MSVC 2015也会产生类似的结果。但是,Intel C++ 15会产生:

neg: -nan(ind)
sub: nan nan 0.000000
add: nan nan
div: nan nan nan
mul: nan nan

具体来说,qNaN - qNaN == 0.0

这...不可能是对的,对吧?相关标准(ISO C, ISO C++, IEEE 754)对此有何规定,为什么编译器之间存在行为差异?


18
JavaScript和Python(NumPy)没有这种行为。 Nan-NaN等于NaN。Perl和Scala的行为也类似。 - Paul
36
你可能启用了不安全的数学优化(相当于 GCC 上的 -ffast-math)? - Matteo Italia
5
不正确。附录F是可选的但规范性强,当得到支持时必须具有浮点行为的规范化,本质上将IEEE 754纳入了C语言。 - R.. GitHub STOP HELPING ICE
5
如果您想询问IEEE 754标准,请在问题中提及。 - n. m.
68
我确定这个问题从标题来看是关于 JavaScript 的。 - Mike G
显示剩余22条评论
3个回答

304

英特尔 C++ 编译器默认的浮点数处理方式为/fp:fast,这种方式不安全地处理NaN(导致例如NaN == NaN的结果为true)。尝试指定/fp:strict/fp:precise参数,看看是否有所帮助。


15
我刚刚尝试了一下。确实,无论是指定 "precise" 还是 "strict" 都可以解决问题。 - geometrian
69
我支持英特尔将默认选项设置为/fp:fast:如果你想要一些安全的东西,最好避免NaN出现在第一位,并且通常不要使用带有浮点数的==。依赖IEEE754指定给NaN的奇怪语义是自讨苦吃。 - leftaroundabout
10
除了 NaN!=NaN 返回 true 的决定太糟糕之外,你还觉得 NaN 有哪些奇怪的地方? - supercat
22
NaNs有重要的用途——它们可以在不需要在每次计算后进行测试的情况下检测异常情况。并非每个浮点数开发人员都需要它们,但不要轻视它们。 - Bruce Dawson
6
@KyleStrand: 不,但如果NaN!=NaN返回true,代码至少可以使用!=来反转逻辑以防止一些问题情况。真正需要的是(出于某种奇怪的原因仍然不普遍可用)一种标准化的高效执行完全有序关系比较的方法。虽然在某些时候,使NaN无序可能很有用,但在其他时候,例如尝试对数字列表进行排序时,它会对所有内容造成严重破坏。如果我设计一种语言,我将拥有多种类型的float和double,它们是相互可转换的... - supercat
显示剩余34条评论

54

这个……可能不对吧?我的问题是:相关标准(ISO C、ISO C++、IEEE 754)对此有何规定?

Petr Abdulin已经解释了编译器为什么会给出0.0的答案。

以下是IEEE-754:2008的规定:

(6.2 NaN的运算)"[...] 对于具有静默NaN输入的操作,除最大值和最小值操作外,如果要生成一个浮点结果,则结果应该是一个静默NaN,它应该是输入NaN之一。"

因此,两个静默NaN操作数相减的唯一有效结果是静默NaN;任何其他结果都是无效的。

C标准如下:

(C11,F.9.2 表达式转换 p1) "[...]

x−x→0.0 "如果x是NaN或无穷大,则表达式x−x和0.0不等价。

(在这里,NaN表示根据F.2.1p1的规定是一个静默NaN "本规范未定义信号NaN的行为。它通常使用术语NaN来表示静默NaN")


21

我看到有人质疑英特尔编译器的标准兼容性,而其他人没有提到这一点,因此我要指出,GCC和Clang都有一个模式,其中它们做了相似的事情。它们的默认行为符合IEEE标准 -

$ g++ -O2 test.cc && ./a.out 
neg: -nan
sub: nan nan nan
add: nan nan
div: nan nan nan
mul: nan nan

$ clang++ -O2 test.cc && ./a.out 
neg: -nan
sub: -nan nan nan
add: nan nan
div: nan nan nan
mul: nan nan

但是如果你为了速度而牺牲正确性,你会得到你所要求的结果。

$ g++ -O2 -ffast-math test.cc && ./a.out 
neg: -nan
sub: nan nan 0.000000
add: nan nan
div: nan nan 1.000000
mul: nan nan

$ clang++ -O2 -ffast-math test.cc && ./a.out 
neg: -nan
sub: -nan nan 0.000000
add: nan nan
div: nan nan nan
mul: nan nan

我认为批评ICC选择的默认值完全是公平的,但我不会把整个Unix战争都归因于那个决定。


请注意,使用“-ffast-math”选项时,gcc不再遵守ISO 9899:2011关于浮点运算的规定。 - fuz
1
@FUZxxl 是的,关键是两者编译器都有不兼容的浮点模式,只是icc默认为该模式而gcc则没有。 - zwol
4
为了添油加醋,我真的很喜欢英特尔默认启用快速数学运算的选择。使用浮点数的整个意义就在于获取高吞吐量。 - Navin

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