提问人:starblue 提问时间:10/14/2009 最后编辑:CSᵠstarblue 更新时间:10/19/2022 访问量:73563
所有比较都对 NaN 值返回 false IEEE754理由是什么?
What is the rationale for all comparisons returning false for IEEE754 NaN values?
问:
为什么 NaN 值的比较行为与所有其他值不同? 也就是说,与运算符 ==、<=、>=、< > 的所有比较,其中一个或两个值都是 NaN 将返回 false,这与所有其他值的行为相反。
我想这在某种程度上简化了数值计算,但我找不到明确说明的原因,甚至在 Kahan 的 Lecture Notes on the Status of IEEE 754 中也没有,其中详细讨论了其他设计决策。
这种异常行为在进行简单的数据处理时会造成麻烦。例如,在对 C 程序中某些实值字段的记录列表进行排序时,我需要编写额外的代码来将 NaN 作为最大元素来处理,否则排序算法可能会变得混乱。
编辑:到目前为止,答案都认为比较 NaN 是没有意义的。
我同意,但这并不意味着正确答案是错误的, 相反,它将是一个非布尔值 (NaB),幸运的是它不存在。
因此,在我看来,选择返回 true 或 false 进行比较是任意的, 对于一般数据处理,如果它遵守通常的法律将是有利的 (==的自反性,<的三分法,==,>), 以免依赖这些定律的数据结构变得混乱。
因此,我要求打破这些定律的一些具体好处,而不仅仅是哲学推理。
编辑2:我想我现在明白了为什么使 NaN 最大化是一个坏主意,它会搞砸上限的计算。
NaN != NaN 可能是可取的,以避免在循环中检测收敛,例如
while (x != oldX) {
oldX = x;
x = better_approximation(x);
}
然而,最好通过将绝对差异与小限制进行比较来编写。 所以恕我直言,这是一个相对较弱的论据,可以打破 NaN 的反身性。
答:
我猜 NaN(不是数字)的意思是:这不是一个数字,因此比较它并没有意义。
这有点像 SQL 中带有操作数的算术:它们都导致 .null
null
浮点数的比较比较数值。因此,它们不能用于非数值。因此,不能在数字意义上比较NaN。
评论
FOO
if (NaN != NaN) foo();
foo
NaN 可以被认为是一个未定义的状态/数字。类似于 0/0 未定义或 sqrt(-3) 的概念(在浮点所在的实数系统中)。
NaN 用作这种未定义状态的占位符。从数学上讲,undefined 不等于 undefined。也不能说一个未定义的值大于或小于另一个未定义的值。因此,所有比较都返回 false。
在将 sqrt(-3) 与 sqrt(-2) 进行比较的情况下,此行为也是有利的。它们都会返回 NaN,但即使它们返回相同的值,它们也不等价。因此,在处理 NaN 时,相等总是返回 false 是理想的行为。
评论
!=
NaN==NaN
NaN!=NaN
==
!=
0/0
NaN
再打个比方。如果我递给你两个盒子,告诉你它们都没有苹果,你会告诉我盒子里装的是同样的东西吗?
NaN 不包含关于某物是什么的信息,只包含它不是什么的信息。因此,这些元素永远不能肯定地说是相等的。
评论
(NaN==Nan)==false
(Nan!=Nan)==true
从维基百科上关于 NaN 的文章中,以下做法可能会导致 NaN:
- 所有数学运算>其中 NaN 至少是一个操作数
- 0/0、∞/∞、∞/-∞、-∞/∞ 和 -∞/-∞
- 乘法 0×∞ 和 0×-∞
- 加法∞ + (-∞)、(-∞) + ∞ 和等效减法。
- 将函数应用于其域之外的参数,包括取负数的平方根、取负数的对数、取 90 度(或 π/2 弧度)的奇数倍的正切,或取小于 -1 或大于 +1 的数字的逆正弦或余弦。
由于没有办法知道这些操作中的哪一个创建了 NaN,因此没有办法比较它们是否有意义。
评论
它之所以看起来很奇怪,是因为大多数允许 NaN 的编程环境也不允许 3 值逻辑。如果将 3 值逻辑放入组合中,它会变得一致:
- (2.7 == 2.7) = 真
- (2.7 == 2.6) = 假
- (2.7 == NaN) = 未知
- (NaN == NaN) = 未知
甚至 .NET 也没有提供运算符,因此您仍然被愚蠢的结果所困扰。bool? operator==(double v1, double v2)
(NaN == NaN) = false
我不知道设计原理,但以下是 IEEE 754-1985 标准的摘录:
“即使操作数的格式不同,也可以比较所有受支持格式的浮点数。比较是准确的,从不溢出或下溢。有四种相互排斥的关系是可能的:小于、相等、大于和无序。当至少一个操作数是 NaN 时,会出现最后一种情况。每个 NaN 都应与无序的事物进行比较,包括它自己。
我是 IEEE-754 委员会的成员,我会尽力帮助澄清一些事情。
首先,浮点数不是实数,浮点算术不满足实数的公理。三分法并不是实数算术中唯一不适用于浮点数的属性,甚至也不是最重要的属性。例如:
- 加法不是关联的。
- 分配律不成立。
- 有没有逆数的浮点数。
我可以继续说下去。不可能指定一个固定大小的算术类型来满足我们所知道和喜爱的实数算术的所有属性。754委员会必须决定弯曲或折断其中一些。这是由一些非常简单的原则指导的:
- 如果可以的话,我们匹配实数算术的行为。
- 当我们做不到时,我们会尽量使违规行为变得可预测且尽可能易于诊断。
关于您的评论“这并不意味着正确答案是错误的”,这是错误的。谓词询问是否小于 。如果是 NaN,则它不小于任何浮点值,因此答案必然是假的。(y < x)
y
x
y
x
我提到过三分法不适用于浮点值。但是,确实有一个类似的属性。754-2008 标准第 5.11 条第 2 款:
有四种相互排斥的关系是可能的:小于、相等、大于和无序。当至少一个操作数是 NaN 时,会出现最后一种情况。每个 NaN 都应将无序与包括它自己在内的所有事物进行比较。
就编写额外的代码来处理 NaN 而言,通常可以(尽管并不总是容易)以这样一种方式构建代码,使 NaN 正确通过,但情况并非总是如此。如果不是这样,可能需要一些额外的代码,但对于代数闭包为浮点运算带来的便利来说,这是一个很小的代价。
补遗: 许多评论者认为,保留平等和三分法的反身性会更有用,因为采用 NaN != NaN 似乎并没有保留任何熟悉的公理。我承认对这种观点有一些同情,所以我想我会重新审视这个答案并提供更多的背景信息。
通过与 Kahan 的交谈,我的理解是 NaN != NaN 源于两个实用的考虑:
只要有可能,这应该等同于(除了作为实算术定理之外,这使得比较的硬件实现更节省空间,这在标准开发时至关重要——但请注意,这违反了 x = y = 无穷大,所以它本身并不是一个很好的理由;它可以合理地弯曲到)。
x == y
x - y == 0
(x - y == 0) or (x and y are both NaN)
更重要的是,当时没有 NaN 在 8087 算术中被形式化的谓词;有必要为程序员提供一种方便有效的方法来检测 NaN 值,而这种方法不依赖于编程语言,这可能需要很多年的时间。我将引用卡汉自己关于这个主题的文章:
isnan( )
isnan( )
如果没有办法摆脱 NaN,它们将像 CRAY 上的无限期一样无用;一旦遇到这种情况,最好停止计算,而不是无限期地持续到无限期的结论。这就是为什么对 NaN 的一些操作必须提供非 NaN 结果的原因。哪些操作?...例外是 C 谓词 “x == x” 和 “x != x”,它们对于每个无限或有限数 x 分别为 1 和 0,但如果 x 不是数字 (NaN),则相反;这些提供了 NaN 和数字之间唯一简单、无例外的区别,这些语言中没有 NaN 的单词和谓词 IsNaN(x)。
请注意,这也是排除返回“Not-A-Boolean”之类的内容的逻辑。也许这种实用主义是错误的,标准应该要求,但这将使 NaN 在世界等待编程语言采用的几年内几乎不可能高效和方便地使用。我不相信这是一个合理的权衡。isnan( )
坦率地说:NaN == NaN 的结果现在不会改变。学会忍受它比在互联网上抱怨要好。如果你想论证适合容器的顺序关系也应该存在,我建议你倡导你最喜欢的编程语言实现 IEEE-754 (2008) 中标准化的谓词。事实上,它还没有说明卡汉的担忧的有效性,这种担忧激发了当前的事态。totalOrder
评论
1f/3f == 10000001f/30000002f
a=b
a
b
a
b
!(x < 0 || x == 0 || x > 0)
x != x
x == NaN
过于简化的答案是 NaN 没有数值,因此它没有什么可以与其他任何东西进行比较。
如果希望 NaN 的行为类似于 +INF,可以考虑测试 NaN 并将其替换为 +INF。
虽然我同意 NaN 与任何实数的比较应该是无序的,但我认为将 NaN 与自身进行比较是有正当理由的。例如,如何发现信号 NaN 和安静 NaN 之间的区别?如果我们将信号视为一组布尔值(即位向量),人们可能会问位向量是相同还是不同,并相应地对集合进行排序。例如,在解码最大偏置指数时,如果有效性向左移动,以便将有效性的最高有效位与二进制格式的最高有效位对齐,则负值将是安静的 NaN,任何正值都是信号 NaN。当然,零是为无穷大保留的,比较将是无序的。MSB对准将允许直接比较信号,即使是来自不同二进制格式的信号。因此,具有相同信号集的两个 NaN 将是等价的,并赋予相等的意义。
因为数学是数字“存在”的领域。 在计算中,您必须初始化这些数字并根据需要保持其状态。 在那些旧时代,内存初始化以您永远无法依赖的方式工作。你永远不能让自己去想这个“哦,那会一直用0xCD初始化,我的算法不会坏”。
因此,您需要适当的非混合溶剂,该溶剂具有足够的粘性,不会让您的算法被吸入和破坏。 涉及数字的好算法大多适用于关系,而那些 if() 关系将被省略。
这只是您可以在创建时将其放入新变量中的润滑脂,而不是从计算机内存中随机编程。你的算法,不管它是什么,都不会崩溃。
接下来,当你仍然突然发现你的算法正在产生 NaN 时,可以将其清理掉,一次查看一个分支。同样,“总是假”规则在这方面有很大帮助。
简短的回答:
因为以下内容:不得持有。否则为 1。
nan / nan = 1
inf/inf
(因此不能等于 。至于 或 ,如果尊重满足阿基米德性质的集合中的任何阶关系,我们将再次处于极限)。nan
nan
>
<
nan
nan / nan = 1
评论
inf = inf
inf / inf = nan
nan = nan
nan / nan = nan
nan / nan = 1
inf/inf
nan
1
MeToo来到这里是为了了解其中的道理,为什么等于假。NaN == NaN
读完(几乎)我仍然感到困惑,为什么不能替换像这样的函数,因为它似乎太明显了。a == NaN
isNaN()
但事情并没有那么简单。
还没有人提到矢量几何。但是许多计算发生在第二维或第三维,所以在向量空间中。
想了一会儿,我立刻意识到,为什么不和自己比较是一件好事。希望其他人也能理解以下。NaN
向量
请耐心等待,需要一段时间才能出现。
NaN
首先,让我为那些对数学没有深入了解的人解释一下
在向量几何中,我们通常使用复数之类的东西。
复数由两个浮点数组成(其中用 表示虚值),这使我们能够解决二维平面上的所有点。对于浮点,我们无法表示每个值,因此我们必须近似一点。因此,如果我们将值四舍五入到我们可以表达的某个值,我们仍然可以尝试创建数值稳定的算法,这为我们提供了一些我们想要存档的内容的良好近似值。a + bi
i
i * i == -1
输入无穷大
这里还没有。请耐心等待。我稍后会在下面谈到这一点。
NaN
如果我们想在很远的地方指定某个点,我们可能会留下我们可以表达的数字范围,这会导致无穷大。在IEEE浮点数中,我们幸运地拥有(我将其写为)或为此(写为)。+inf
inf
-inf
-inf
这很好:
a + inf i
有道理,对吧?它是 x 轴上某个点的向量,y 轴上的某个点的位置是“正无穷大”。但是等一下,我们在这里谈论的是矢量!a
向量具有原点和指向的点。归一化向量是从位置开始的向量。(0,0)
现在考虑一个原点指向 的向量。(0,0)
(a,inf)
还有道理吗?差一点。当我们仔细观察时,我们会发现,归一化向量是相同的向量!由于向量太长,无法再看到无穷大的推导。或者不这么说:(0,inf)
a
对于笛卡尔坐标系中的不定式长向量,有限轴可以表示为 ,因为我们被允许近似(如果不允许我们近似,我们就不能使用浮点!0
所以替换向量仍然适用。事实上,任何一个都是有限 .那么,为什么不从我们的归一化向量的来源使用呢?(0,inf)
(x,inf)
x
0
因此,我们在这里得到了什么?好吧,如果允许我们的向量,我们实际上得到了 8 个可能的无限向量,每个向量旋转 45 度(括号中的度数):inf
(inf,0)
(0)、(45)、(90)、(135)、(180)、(225)、(270)和(315)(inf,inf)
(0,inf)
(-inf,inf)
(-inf,0)
(-inf,-inf)
(0,-inf)
(inf,-inf)
所有这些都不会造成任何麻烦。事实上,能够表达的不仅仅是有限向量,这是件好事。这样,我们就有了模型的自然扩展。
极坐标
这里仍然没有,但我们越来越近了
NaN
上面我们使用复数作为笛卡尔坐标。但是复数也有第二种选择,我们如何写它们。那就是极坐标。
极坐标由长度和角度组成,如 。因此,如果我们将复数转换为极坐标,我们将看到,我们可以表达的不仅仅是 8 个角。[angle,length]
[angle,inf]
因此,如果你想创建一个数学模型,允许在某些多维空间中使用无限长的向量,你肯定希望在计算中尽可能多地使用极坐标。
为此,您所要做的就是将笛卡尔坐标转换为极坐标,反之亦然。
如何做到这一点留给读者作为练习。
进入NaN
现在,我们有什么?
- 我们有一个用极坐标计算的数学模型。
- 我们有一些输出设备,可能使用笛卡尔坐标。
我们现在要做的是能够在这两者之间进行转换。我们需要什么?
当然,我们需要浮点运算!
由于我们可能需要用几万亿个坐标进行计算,(也许我们渲染了一些天气预报或从大型强子对撞机中获得了一些碰撞数据),我们不想包括缓慢且容易出错的错误处理(WTF?容易出错的错误处理?你敢打赌!在所有这些复杂的数学(希望数字稳定)步骤中。
那么我们如何传播错误呢?
好吧,正如IEEE所说:我们使用NaN
进行误差传播
那么我们在这里有什么?
- 极坐标空间中的一些计算
- 一些转换为笛卡尔空间
- NaN 作为失败时的救援
然后导致......
..为什么一定是假的NaN == NaN
为了解释这一点,让我们首先将这个复杂的东西简化为笛卡尔坐标中 2 个向量的简单结果:
(a,b)
和(c,d)
我们想比较这两者。这是这个比较的样子:
a == c && b == d
到目前为止一切正确吗?
是的。但前提是我们观察到以下两个极向量,它们可能是我们两个笛卡尔向量的来源:
[NaN,inf]
和[0,NaN]
当然,这两个向量在极坐标空间中是不相等的。但是在转换为笛卡尔空间之后,两者都是:
(NaN,NaN)
和(NaN,NaN)
那么,他们应该突然平等吗?
当然不是!
多亏了 IEEE 定义 that must return ,我们非常原始的向量比较仍然给了我们预期的结果!NaN == NaN
false
我认为,这正是IEEE定义它背后的动机。
现在我们必须忍受这个烂摊子。但这确实是一团糟吗?我犹豫不决。但是,至少,我现在能理解(可能的)推理。
希望我没有错过什么。
一些遗言
当涉及到浮点数时,比较事物的原始方法通常并不完全合适。
在浮点中,您通常不使用 ,而是使用一些非常小的值。这是因为已经类似的东西可能不是真的,这取决于你运行的硬件。==
abs(a-b) < eps
eps
1/3 + 1/3 * 2.0 == 1.0
1/3 + 1/3 * 2.0 == 1/3 + 1/3 + 1/3
在所有合理的硬件上都应该为 true。所以甚至可以使用。只有小心。但不排除。==
然而,这并不意味着上述推理无效。因为以上并不能从数学上证明IEEE是正确的。这只是一个例子,它应该允许理解背后的推理来源,以及为什么以这种方式定义它可能更好。
即使它是像我这样的所有编程人员的 PITA。
评论
while (fabs(x - oldX) > threshold)