NaN 的位模式真的依赖于硬件吗?

2022-08-31 16:26:38

我正在阅读Java语言规范中的浮点NaN值(我很无聊)。32 位具有以下位格式:float

seee eeee emmm mmmm mmmm mmmm mmmm mmmm

s是符号位,是指数位,是尾数位。NaN 值被编码为所有 1 的指数,并且尾数位并不都是 0(这将是 +/- 无穷大)。这意味着有许多不同的可能的NaN值(具有不同的值和位值)。emsm

关于这一点,JLS §4.2.3说:

IEEE 754 允许其每种单浮点和双浮点格式使用多个不同的 NaN 值。虽然每个硬件架构在生成新的NaN时都会为NaN返回一个特定的位模式,但程序员也可以创建具有不同位模式的NaN,以编码,例如,回顾性诊断信息。

JLS 中的文本似乎暗示,例如,的结果具有与硬件相关的位模式,并且根据该表达式是否作为编译时常量计算,它所依赖的硬件可能是编译 Java 程序的硬件或运行程序的硬件。如果这是真的,这一切似乎都非常片面。0.0/0.0

我运行了以下测试:

System.out.println(Integer.toHexString(Float.floatToRawIntBits(0.0f/0.0f)));
System.out.println(Integer.toHexString(Float.floatToRawIntBits(Float.NaN)));
System.out.println(Long.toHexString(Double.doubleToRawLongBits(0.0d/0.0d)));
System.out.println(Long.toHexString(Double.doubleToRawLongBits(Double.NaN)));

我的计算机上的输出是:

7fc00000
7fc00000
7ff8000000000000
7ff8000000000000

输出未显示任何超出预期的内容。指数位均为 1。尾数的上位也是1,对于NaNs来说,这显然表示“安静的NaN”,而不是“信号NaN”(https://en.wikipedia.org/wiki/NaN#Floating_point)。符号位和尾数位的其余部分为 0。输出还显示,我的机器上生成的 NaN 与 Float 和 Double 类中的常量 NaN 之间没有差异。

我的问题是,无论编译器或VM的CPU如何,Java中的输出是否得到保证,还是这一切都是不可预测的?JLS对此很神秘。

如果该输出是有保证的,是否有任何算术方法可以生成具有其他(可能依赖于硬件的?)位模式的NaN?(我知道/可以编码其他NaN,但我想知道其他值是否可以从正常算术中出现。0.0/0.0intBitsToFloatlongBitsToDouble


后续点:我注意到 Float.NaNDouble.NaN 指定了它们的确切位模式,但在源代码(FloatDouble)中,它们是由 .如果这种划分的结果真的取决于编译器的硬件,那么在规范或实现中似乎都存在缺陷。0.0/0.0


答案 1

这就是 JVM 7 规范的 §2.3.2 对它的看法:

双精度值集的元素正是可以使用 IEEE 754 标准中定义的双浮点格式表示的值,只是只有一个 NaN 值(IEEE 754 指定 253-2 个不同的 NaN 值)。

§2.8.1

Java 虚拟机没有信令 NaN 值。

所以从技术上讲,只有一个NaN。但JLS的§4.2.3也说(就在你的报价之后):

在大多数情况下,Java SE 平台将给定类型的 NaN 值视为折叠为单个规范值,因此此规范通常将任意 NaN 视为规范值。

但是,Java SE平台的1.3版本引入了使程序员能够区分NaN值的方法:Float.floatToRawIntBits和Double.doubleToRawLongBits方法。有兴趣的读者可以参考 Float 和 Double 类的规范以获取更多信息。

我认为这完全符合你和CandiedOrange的建议:它依赖于底层处理器,但Java对待它们都是一样的。

但它变得更好了:显然,你的NaN值完全有可能被静默地转换为不同的NaNs,如Double.longBitsToDouble()中所述:

请注意,此方法可能无法返回与 long 参数具有完全相同位模式的双 NaN。IEEE 754 区分了两种 NaN,即静音 NaN 和信令 NaN。这两种NaN之间的差异在Java中通常不可见。信令 NaNs 上的算术运算将它们转换为具有不同但通常相似的位模式的安静 NaN。但是,在某些处理器上,仅复制信令NaN也会执行该转换。特别是,复制信令NaN以将其返回到调用方法可以执行此转换。因此,longBitsToDouble 可能无法返回具有信令 NaN 位模式的双精度值。因此,对于某些长值,doubleToRawLongBits(longBitsToDouble(start))可能不等于 start。此外,哪些特定的位模式表示信令NaNs取决于平台;尽管所有NaN位模式,安静或信令,必须在上面标识的NaN范围内。

作为参考,此处提供了与硬件相关的 NaN 的表。综上所述:

- x86:     
   quiet:      Sign=0  Exp=0x7ff  Frac=0x80000
   signalling: Sign=0  Exp=0x7ff  Frac=0x40000
- PA-RISC:               
   quiet:      Sign=0  Exp=0x7ff  Frac=0x40000
   signalling: Sign=0  Exp=0x7ff  Frac=0x80000
- Power:
   quiet:      Sign=0  Exp=0x7ff  Frac=0x80000
   signalling: Sign=0  Exp=0x7ff  Frac=0x5555555500055555
- Alpha:
   quiet:      Sign=0  Exp=0      Frac=0xfff8000000000000
   signalling: Sign=1  Exp=0x2aa  Frac=0x7ff5555500055555

因此,要验证这一点,您确实需要其中一个处理器并尝试一下。此外,欢迎任何关于如何解释 Power 和 Alpha 架构的较长值的见解。


答案 2

我在这里阅读JLS的方式,NaN的确切位值取决于谁/什么创造了它,因为JVM没有制造它,所以不要问他们。您也可以问他们“错误代码4”字符串是什么意思。

硬件产生不同的位模式,旨在表示不同类型的NaN。不幸的是,不同种类的硬件为相同种类的NaN产生不同的位模式。幸运的是,有一个标准模式,Java可以使用它至少来判断它是某种NaN。

这就像Java看着“错误代码4”字符串说:“我们不知道'代码4'在你的硬件上意味着什么,但这个字符串中有'错误'这个词,所以我们认为这是一个错误。

不过,JLS试图给你一个机会自己弄清楚它:

“然而,Java SE平台的1.3版本引入了一些方法,使程序员能够区分NaN值:和方法。有兴趣的读者可以参考 和 类 的规范以获取更多信息。Float.floatToRawIntBitsDouble.doubleToRawLongBitsFloatDouble

在我看来,这就像一个C++。Java为您提供了自己分析NaN的机会,以防您碰巧知道其信号是如何编码的。如果你想跟踪硬件规格,这样你就可以预测哪些不同的事件应该产生哪些NaN位模式,你可以自由地这样做,但你超出了JVM应该给我们的一致性。因此,预计它可能会从硬件更改为硬件。reinterpret_cast

在测试一个数字是否是NaN时,我们会检查它是否等于它自己,因为它是唯一一个不等于NaN的数字。这并不是说这些位是不同的。在比较位之前,JVM 会测试许多位模式,这些模式说它是 NaN。如果它是这些模式中的任何一个,那么它就会报告它不相等,即使两个操作数的位确实相同(即使它们不同)。

早在1964年,当被要求为色情作品给出一个确切的定义时,美国最高法院大法官斯图尔特(Stewart)有一句名言:“当我看到它时,我就知道它”。我认为Java对NaN做了同样的事情。Java无法告诉你任何“信令”NaN可能正在信令的事情,因为它不知道该信号是如何编码的。但它可以查看这些位并告诉它是某种NaN,因为该模式遵循一个标准。

如果你碰巧在用统一位对所有NaN进行编码的硬件上,你永远不会证明Java正在做任何事情来使NaN具有统一位。再一次,我阅读JLS的方式,他们直截了当地说你在这里独自一人。

我能理解为什么这感觉很片状。它是片状的。这不是Java的错。我敢说,一些有进取心的硬件制造商提出了极具表现力的信令NaN位模式,但他们未能将其广泛采用为Java可以信赖的标准。这就是片状的东西。我们保留了所有这些位,用于表明我们拥有哪种NaN,并且不能使用它们,因为我们不同意它们的含义。在硬件使它们成为统一值之后,让Java击败NaN只会破坏这些信息,损害性能,唯一的回报就是看起来不片状。鉴于这种情况,我很高兴他们意识到他们可以欺骗解决问题,并将NaN定义为不等于任何东西。