正负NaN的区别

7

我有一些数值代码,是在AMD64 Linux下使用LLVM 3.2开发的。

我最近将它移植到了带有XCode的OSX 10.9上。它可以正常运行,但它失败了很多单元测试:似乎有些计算在Linux上返回NaN(或-NaN),而现在在OSX上返回-NaN(或NaN)。

我是否可以安全地假定正NaN和负NaN是等价的,并调整我的单元测试以接受任何一种作为成功的标准,还是这是某些更严重问题的迹象?


如果您的NaN具有符号,那么可能已经出现了严重问题。这不是正常的情况。 - user2357112
https://dev59.com/f1_Va4cB1Zd3GeqPQidR#8817304 - bobah
哦,我以为符号位总是被忽略的;看来有些系统会显示它。那应该没问题了。 - user2357112
你的测试区分不同的NaN有点奇怪。如果x和/或y是一个NaN,那么x == yx < yx > y都会返回false;通过数值比较无法区分NaN。这表明你的测试要么检查代表NaN的位,要么检查NaN的某些转换,比如使用printf打印NaN时产生的字符。在前一种情况下,有人已经决定了哪些位是重要的,你应该理解为什么。在后一种情况下,你依赖于printf的实现相关属性。 - Eric Postpischil
是的,这些测试只是在比较 printf 的输出。当然,我知道我需要理解哪些位是重要的——这就是我提出问题的原因! - David Given
2个回答

19
在IEEE-754算术中,不存在“负NaN”的概念。 NaN编码仍然具有一个符号比特,存在一种“符号位”操作,可使用或影响此比特(copysign、abs和其他一些操作),但当将NaN编码解释为值时,它没有任何意义。许多打印例程通常会将该比特打印为负号,但这在形式上是无意义的,因此标准中没有太多规定其值应该是什么(除了那些前述的函数)。
以下是IEEE-754(2008年版)的相关部分:
将支持格式中的安静NaN转换为外部字符序列应产生语言定义之一的“nan”或一个等效于其的序列,不同之处仅为大小写(例如,“NaN”),并带有一个可选的前导符号。(本标准不解释NaN的符号。)
因此,您平台的转换函数可能会打印NaN值的“符号”,但它没有实际含义,在测试目的上您不应该考虑它。
修订后更为强烈:附加意义于NaN数据的“符号比特”几乎总是出现错误。

1
然而,第一个系统上有“NaN”结果,第二个系统上有“-NaN”结果,以及第一个系统上有“-NaN”结果,第二个系统上有“NaN”结果的事实表明,在早期的浮点运算中发生了一些不同的事情,而不仅仅是最终的printf。在生成NaN之前,可能存在两个系统上符号不同的数值结果。测试程序未识别数值结果(非NaN结果)中的错误可能是因为测试不足。因此,值得调查为什么符号不同。 - Eric Postpischil
3
更可能的解释是,有一个库函数在一个平台上保留NaN的“符号”,而在另一个平台上则不保留(这是可以接受的,因为“符号”没有意义)。这很常见;一个平台有 if (isnan(x)) return x,另一个平台有 if (isnan(x)) return NAN 或类似语句。唯一需要进一步调查的情况是,被测试的例程只包含基本算术运算(没有库函数调用),并且在完全相同的硬件上运行。 - Stephen Canon
@supercat:是的(但+0和-0的加法是可交换的)。 - Stephen Canon
根据WIKI,(+0)+(-0)的结果为(+0),但(-0)+(+0)的结果为(-0)。我认为这样的规则很愚蠢,因为仅仅为了一个极其狭窄且通常不相关的角落案例而使加法非交换,这导致几乎在任何地方都不能交换,但这并不像无法定义任何形式的比较那样糟糕,可以单独使用作为等价关系的最佳方法是!((a < b) || (a > b)),但这很棘手。 - supercat
@supercat:恐怕维基百科是在胡说八道。根据 IEEE-754 委员会成员(即我)的说法,无论顺序如何,(+0) + (-0) 均为 +0,除非舍入模式为向负无穷舍入,在这种情况下它就是 -0。 - Stephen Canon
显示剩余6条评论

4

这完全取决于你的单元测试在测试什么。

除非你正在测试IEEE754浮点软件本身或打印它们的C运行时代码,否则大多数情况下你可以将它们视为相等的。否则,如果使用你正在测试的内容的代码将它们视为相同,则应将它们视为相同。

这是因为测试应该反映出你的真实用途,在每种情况下都如此。一个(虽然有些牵强)的例子是,如果你正在测试返回double的函数doCalc(),并且它只被用于以下情况:

x = doCalc()
if x is any sort of Nan:
    doSomethingWithNan()

如果您的测试应该将所有NaN值视为等同,则可以这样使用它。但是,如果您像这样使用它:

x = doCalc()
if x is +Nan:
    doSomethingForPositive()
else:
    if x is -Nan:
        doSomethingForNegative()

如果你希望将它们视为不同的内容,则需要进行区分。

同样地,如果你的实现在小数位上创建了有用的负载(请参见下文),并且你的真实代码使用了它,那么它也应该由单元测试来检查。


由于NaN只是指数中的所有1位和分数中除了所有零位之外的其他值,因此符号位可以是正或负,而分数位可以是各种各样的值。然而,它仍然是超出数据类型表示范围的一个值或结果,所以如果你期望得到这个值,那么符号或负载包含什么可能并没有太大的区别。

关于检查NaN值的文本输出,在NaN的维基百科页面中指出,不同的实现可能会给出非常不同的输出,其中包括:

nan
NaN
NaN%
NAN
NaNQ
NaNS
qNaN
sNaN
1.#SNAN
1.#QNAN
-1.#IND

甚至还有变体显示其NaN性质没有影响的符号和有效载荷:

-NaN
NaN12345
-sNaN12300
-NaN(s1234)

因此,如果您想在单元测试中实现高度可移植性,您会注意到除了一个输出表示之外,所有的输出表示都有某种形式的字符串nan。因此,对值进行不区分大小写的搜索以查找字符串nanind,就可以捕获它们。这可能在所有环境中都不起作用,但覆盖范围非常广。
值得一提的是,C标准对使用%f%F使用大写字母)输出浮点数值有以下说明:

表示NaNdouble参数会以[-]nan[-]nan(n-char-sequence)其中一种方式转换,具体是哪种方式以及任何n-char-sequence的含义都由实现定义。

因此,简单地检查值是否包含nan即可。

然而,那并没有完全回答我的问题 --- 我需要知道 NaN 中的符号位是否足够重要,以至于我的单元测试需要注意它。我真的很惊讶 OSX 和 Linux 在这里产生了不同的结果:它们使用的是相同的处理器和编译器,而且我认为 IEEE 浮点规范没有任何余地在这里产生不同的结果。这是我需要关心的事情吗? - David Given
@David,如我所说,这取决于你的测试。除非你特意测试NaN的输出,否则几乎可以肯定所有的NaN都是相同的,因为任何找到一个NaN的代码几乎肯定会以同样的方式处理它们。如果你正在测试一个函数,其调用者将根据符号而有所不同的行为,那么你应该以_不同_的方式对待它们。但这是非常不寻常的。我会澄清的。 - paxdiablo
我并不是说符号“不存在”或“没有可观察的效果”;我是说任何类似于你第二个代码片段的程序都有一个错误,因此几乎从来没有一个好的理由将它们视为不同的。 - Stephen Canon
那么,在什么情况下将它们视为不同是很重要的呢?一定有一些情况,否则printf就不会将它们区分开来。 - David Given
1
printf 可以因多种原因显式地呈现它们:因为标准允许,并且该行为恰好从实现中自然派生出来,或者因为编写有关格式说明符的转换程序的人不知道没有理由区分它们,或者因为它们具有某些特定的用途,这与其需求相关,但超出了标准的范围。IEEE-754 NaN 的符号位没有语义意义。 - Stephen Canon
显示剩余2条评论

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