IEEE754 NaN值所有比较返回false的基本原理是什么?

为什么NaN值的比较与所有其他值的行为不同? 也就是说,与运算符==,<=, >=, <, >的所有比较,其中一个或两个值都是NaN,返回false,与所有其他值的行为相反

我认为这在某种程度上简化了数值计算,但我找不到一个明确的理由,甚至在Kahan的IEEE 754现状讲座讲稿中也没有详细讨论其他设计决策。

在进行简单的数据处理时,这种异常行为会造成麻烦。例如,当在C程序中对记录列表w.r.t.某个实值字段进行排序时,我需要编写额外的代码来处理NaN作为最大元素,否则排序算法可能会变得混乱。

<强>编辑: 到目前为止,所有的答案都认为比较nan是没有意义的

我同意,但这并不意味着正确答案是错误的, 相反,它将是一个Not-a-Boolean (NaB),幸运的是它不存在 所以在我看来,选择返回true或false进行比较是任意的, 对于一般的数据处理来说,如果它符合通常的规律,那将是有利的 (==的反身性,<, ==, >), 以免依赖于这些定律的数据结构变得混乱

所以我要求的是打破这些定律的一些具体好处,而不仅仅是哲学推理。

<强>编辑2: 我想我现在明白了为什么让NaN最大是一个坏主意,它会搞砸上限的计算

NaN != NaN可能是可取的,以避免检测循环中的收敛,例如

while (x != oldX) {
oldX = x;
x = better_approximation(x);
}

,但最好是通过比较小的极限的绝对差来写。 所以恕我直言,这是在NaN处打破自反性的一个相对较弱的论据

67440 次浏览

我猜NaN(不是一个数字)的意思就是:这不是一个数字,因此比较它是没有意义的。

这有点像带有null操作数的SQL中的算术:它们的结果都是null

浮点数的比较比较数值。因此,它们不能用于非数值值。因此,NaN不能在数字意义上进行比较。

NaN可以被认为是一个未定义的状态/数。类似于0/0未定义或根号(-3)的概念(在浮点数所在的实数系统中)。

NaN被用作这种未定义状态的一种占位符。从数学上讲,未定义并不等于未定义。你也不能说一个未定义值大于或小于另一个未定义值。因此,所有比较返回false。

这种行为在比较根号(-3)和根号(-2)的情况下也很有利。它们都会返回NaN,但它们并不等效,即使它们返回相同的值。因此,在处理NaN时,具有相等总是返回false是理想的行为。

再打个比方。如果我给你两个盒子,告诉你两个盒子里都没有苹果,你会告诉我两个盒子里装的是同样的东西吗?

NaN不包含某物是什么,只包含它不是什么。因此,这些元素不能肯定地说相等。

根据wikipedia关于的文章,以下做法可能会导致nan:

  • 以NaN为至少一个操作数的所有数学运算>
  • 分为0/0、∞/∞、∞/-∞、-∞/-∞和-∞/-∞
  • 0×∞和0×-∞的乘法
  • 加上∞+(-∞)(-∞)+∞和等价的减法。
  • 将函数应用于其域外的参数,包括取负数的平方根,取负数的对数,取90度(或π/2弧度)的奇数倍的正切,或取小于-1或大于+1的数字的反正弦或余弦。

由于无法知道这些操作中的哪一个创建了NaN,因此无法对它们进行有意义的比较。

它看起来很奇怪,因为大多数允许nan的编程环境也不允许3值逻辑。如果你加入3值逻辑,它就会变得一致:

  • (2.7 == 2.7) = true
  • (2.7 == 2.6) = false
  • (2.7 == NaN) =未知
  • (NaN == NaN) =未知

即使。net也没有提供bool? operator==(double v1, double v2)操作符,所以你仍然会被愚蠢的(NaN == NaN) = false结果所困扰。

我不知道其设计原理,但以下是IEEE 754-1985标准的摘录:

应该能够比较所有支持格式的浮点数,即使操作数的格式不同。比较是精确的,不会溢出也不会溢出。可能存在四种互斥关系:小于、等于、大于和无序。当至少有一个操作数是NaN时,会出现最后一种情况。每个NaN都应该与一切事物,包括它自己,进行无序比较。”

我是IEEE-754委员会的成员,我会试着帮助澄清一些事情。

首先,浮点数不是实数,浮点算术不满足实数算术的公理。三分并不是真正算术中对浮点数不成立的唯一性质,甚至也不是最重要的性质。例如:

  • 加法不是结合律。
  • 分配律不成立。
  • 有不带倒数的浮点数。

我还可以继续列举。不可能指定一个固定大小的算术类型来满足我们所知道和喜爱的实算术的所有属性。754委员会必须决定改变或打破其中的一些。这是由一些非常简单的原则指导的:

  1. 当我们可以的时候,我们匹配真实算术的行为。
  2. 当我们做不到的时候,我们会尽量让违规行为变得可预测,并且尽可能容易诊断。

关于你所说的“这并不意味着正确答案是错误的”,这是错误的。谓词(y < x)询问y是否小于x。如果y为NaN,则其值小于任何浮点值x,因此答案必然为假。

我提到过三分法不适用于浮点值。然而,有一个类似的性质是成立的。754-2008标准第2段第5.11条:

可能存在四种互斥关系:小于、等于、大于和无序。当至少有一个操作数是NaN时,会出现最后一种情况。每个NaN都应该与包括自身在内的所有事物进行无序比较。

就编写额外的代码来处理nan而言,通常有可能(尽管并不总是容易)以正确的方式构建代码以使nan失败,但情况并非总是如此。如果不是,则可能需要一些额外的代码,但这只是代数闭包为浮点算术带来的便利所付出的小小代价。


< p >附录: 许多评论者认为,保留等式的反身性和三分法会更有用,因为采用NaN != NaN似乎并没有保留任何熟悉的公理。我承认我对这个观点有一些同情,所以我想我应该重新审视这个答案,并提供更多的背景

通过与Kahan的交谈,我的理解是NaN != NaN起源于两个务实的考虑:

  • x == y应该尽可能等价于x - y == 0(除了作为一个真正的算术定理之外,这使得比较的硬件实现更节省空间,这在标准开发时是至关重要的——然而,请注意,这违反了x = y =无穷大,所以这本身不是一个很好的理由;它可以被合理地弯曲为(x - y == 0) or (x and y are both NaN))。

  • 更重要的是,在8087算术中NaN形式化时,没有isnan( )谓词;有必要为程序员提供一种方便有效的方法来检测NaN值,而不依赖于编程语言提供诸如isnan( )之类的东西,这可能需要很多年。我将引用Kahan自己关于这个主题的文章:

如果没有办法摆脱nan,它们就会像cray上的不定项一样无用;一旦遇到一个,计算最好停止,而不是继续无限的时间,直到一个无限的结论。这就是为什么对nan的一些操作必须交付非nan结果的原因。哪些操作?例外是C谓词“x == x”和“x != x”,对于每个无限或有限数x分别为1和0,但如果x不是一个数字(NaN)则相反;在缺少NaN和谓词IsNaN(x)的语言中,它们提供了NaN和数字之间唯一简单的、正常的区别。

请注意,这也是排除返回“Not-A-Boolean”之类内容的逻辑。也许这种实用主义是错误的,标准应该要求isnan( ),但这将使NaN在世界等待编程语言采用的几年里几乎不可能有效和方便地使用。我不相信这是一个合理的权衡。

坦率地说:NaN == NaN的结果现在不会改变。与其在网上抱怨,不如学着接受现实。如果你想论证适合容器的顺序关系应该存在,我建议提倡你最喜欢的编程语言实现IEEE-754(2008)标准化的totalOrder谓词。事实上,它还没有证明卡汉的担忧是正确的,正是这种担忧促使了目前的事态。

过于简单的回答是,NaN没有数值,因此其中没有任何东西可以与其他任何东西进行比较。

如果您希望nan像+INF一样工作,您可以考虑测试并使用+INF替换nan。

虽然我同意NaN与任何实数的比较都应该是无序的,但我认为有理由将NaN与自身进行比较。例如,如何发现信号nan和安静nan之间的区别?如果我们把信号看作一组布尔值(即位向量),人们可能会问这些位向量是相同的还是不同的,并相应地排序。例如,在解码最大偏置指数时,如果将显著值左移,以便将显著值的最显著位对齐到二进制格式的最显著位上,则负值将是静态NaN,任何正值将是信号NaN。0当然是为无穷保留的,而且比较是无序的。MSB对齐将允许直接比较来自不同二进制格式的信号。因此,具有相同信号集的两个nan将是等效的,并赋予相等的含义。

因为数学是数字“仅仅存在”的领域。 在计算中,你必须根据需要初始化这些数字并保持它们的状态。 在过去的日子里,内存初始化的工作方式是你永远无法依赖的。你永远不可能让自己想想这个"哦,那会一直用0xCD初始化,我的算法不会坏的"。< / p > 所以你需要适当的non-mixing溶剂,即粘性足够,以防止你的算法被卷入和破坏。 涉及数字的优秀算法大多使用关系,而那些如果()关系将被省略

这只是油脂,你可以在创建新变量,而不是从计算机内存编程随机地狱。不管你的算法是什么,都不会崩溃。

接下来,当您仍然突然发现您的算法正在生成nan时,可以将其清除,一次查看每个分支。同样,“总是错误”的规则在这方面很有帮助。

很简单的回答:

原因如下: Nan / Nan = 1 绝对不能持有。否则inf/inf将为1

(因此nan不能等于nan。对于><,如果nan尊重满足阿基米德性质的集合中的任何顺序关系,我们将再次在极限处得到nan / nan = 1)。

MeToo来这里是为了理解为什么NaN == NaN等于假。

读完(几乎)所有的内容后,我仍然感到困惑,为什么a == NaN不能取代isNaN()这样的函数,因为它似乎是如此明显。

但事情并没有那么简单。

还没有人提到矢量几何。但很多计算是在2维或3维中进行的,所以在向量空间中。

在思考了这一点之后,我立即意识到,为什么NaN不与自身比较是一件好事。希望其他人也能很容易理解下面的内容。

向量

请容忍我,直到NaN显示需要一段时间。

首先让我为那些不太懂数学的人解释一下

在向量几何中,我们通常使用复数。

一个复数由两个浮点a + bi组成(其中i表示虚数,i * i == -1表示虚数),这允许我们在二维平面上寻址所有点。使用浮点数,我们不能表示每个值,所以我们必须近似地表示一个位。因此,如果我们将这些值四舍五入到我们可以表示的某个值,我们仍然可以尝试创建数值稳定的算法,这将为我们提供一些我们想要存档的东西的良好近似。

进入无穷

这里还没有NaN。请耐心等待。稍后我将在下面谈到这一点。

如果我们想指定一个很远很远的点,我们可以留下我们可以表示的数字的范围,结果是无穷大。在IEEE浮点中,我们幸运地有+inf(我将其写成inf)或-inf(写成-inf)。

这很好:

a + inf i是有意义的,对吗?它是指向x轴上a位置和y轴上“正无穷”位置的某个点的向量。等一下,我们说的是带菌者!

向量有原点和指向点。归一化向量是指从位置(0,0)开始的向量。

现在考虑一个原点为(0,0)的向量,指向(a,inf)

还说得通吗?不完全是。当我们仔细观察时,我们会发现规范化向量(0,inf)是同一个向量!由于向量太长,在无穷中无法再看到a的推导。或者换种说法:

对于笛卡尔坐标系中的无限长向量,有限轴可以表示为0,因为我们可以进行近似(如果不允许近似,就不能使用浮点数!)

所以替换向量(0,inf)仍然是合适的。事实上,任何(x,inf)都是有限x的合适替代品。所以为什么不使用归一化向量原点的0呢?

那么我们得到了什么?好吧,在我们的向量中允许inf,我们实际上得到了8个可能的无限向量,每个都旋转了45度(括号中是度):

(inf,0) (0) (inf,inf) (45), (0,inf) (90), (-inf,inf) (135), (-inf,0) (180), (-inf,-inf) (225), (0,-inf)(270)和(inf,-inf) (315)

这一切都没有造成任何麻烦。事实上,能够表达有限向量以外的东西是很好的。这样我们就可以自然地扩展我们的模型。

极坐标

这里仍然没有NaN,但是我们越来越接近了

上面我们用复数作为笛卡尔坐标。但是复数还有第二种写法。这就是极坐标。

极坐标由长度和角度组成,如[angle,length]。因此,如果我们将复数转换到极坐标中,我们将看到,我们可以在[angle,inf]中表示比8个角更多一点的角。

因此,如果你想创建一个数学模型,它允许在某个多维空间中无限长的向量,你肯定想在你的计算中尽可能地使用极坐标。

你所要做的就是把笛卡尔坐标转换成极坐标,反之亦然。

如何做到这一点是留作读者练习

进入NaN

现在,我们有什么?

  • 我们有一个用极坐标计算的数学模型。
  • 我们有一些输出设备,可能使用笛卡尔坐标。

我们现在要做的是能够在这两者之间进行转换。我们需要做什么?

当然,我们需要浮点数!

由于我们可能需要计算一些千万亿的坐标,(也许我们要渲染一些天气预报,或者从大型强子对撞机获得一些碰撞数据),我们不希望包括缓慢且容易出错的错误处理(WTF?容易出错的错误处理?在所有这些复杂的数学(希望数值稳定)步骤中。

那么我们如何传播误差呢?

好吧,正如IEEE所说:我们使用NaN进行错误传播

这是什么情况呢?

  • 在极坐标空间中的一些计算
  • 转换到笛卡尔空间
  • 如果有什么事情失败了,我不会去营救

这就导致了。

.. 为什么NaN == NaN必须为假

为了解释这一点,让我们先将这个复杂的东西简化为笛卡尔坐标下2个向量的简单结果:

  • (a,b)(c,d)

我们想比较这两个。这是这个比较的样子:

  • a == c && b == d

到目前为止一切都正确吗?

是的。但直到我们观察到以下两个极向量它们可能是我们的两个笛卡尔向量的来源:

  • [NaN,inf][0,NaN]

当然这两个向量在极坐标空间中不相等。但转换到笛卡尔空间后,两者都是:

  • (NaN,NaN)(NaN,NaN)

那么,它们应该突然相等比较吗?

肯定不是!

由于IEEE定义了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应该在所有合理的硬件上为真。所以即使是==也可以使用。只有仔细。但不排除这种可能性。

然而,这并不意味着上述推理无效。因为上面并不是一个数学证明,IEEE是正确的。这只是一个例子,它可以让你理解背后的原因,以及为什么最好这样定义它。

即使它是一个面向所有像我这样的编程人员的PITA。