NaN的位模式真的与硬件相关吗?

61

我在阅读Java语言规范中关于浮点NaN值的部分(我很无聊)。一个32位的float具有以下比特格式:

seee eeee emmm mmmm mmmm mmmm mmmm mmmm

s代表符号位,e代表指数位,m代表尾数位。NaN值被编码为全1的指数位和不全是0的尾数位(否则将为+/-无穷大)。这意味着有很多可能的不同的NaN值(具有不同的sm位值)。

在此,JLS §4.2.3表示:

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

JLS 中的文本似乎暗示了例如 0.0/0.0 的结果具有依赖于硬件的位模式,并且取决于该表达式是否计算为编译时常量,它所依赖的硬件可能是 Java 程序编译所在的硬件或运行程序所在的硬件。如果这是真的,那么所有这些都似乎非常不靠谱。

我运行了以下测试:

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,对于NaN来说显然表示“静默NaN”,而不是“信号NaN” (https://en.wikipedia.org/wiki/NaN#Floating_point)。符号位和剩余的尾数位都为0。输出还显示,在我的计算机上生成的NaN和Float和Double类中的常量NaN之间没有区别。

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

如果对于0.0/0.0,这种输出是有保证的话,是否有任何算术方法可以产生具有其他(可能依赖于硬件)比特模式的NaN?(我知道intBitsToFloat/longBitsToDouble可以编码其他NaN,但我想知道是否可以从正常的算术运算中出现其他值。)


后续观点:我注意到Float.NaNDouble.NaN指定了它们的确切比特模式,但在源代码(FloatDouble)中,它们是通过0.0/0.0生成的。如果该除法的结果确实取决于编译器的硬件,那么规范或实现中可能存在缺陷。


3
尝试在IBM iSeries上测试它。不幸的是,我目前没有可用的设备。 - Elliott Frisch
4个回答

39

这是JVM 7规范的§2.3.2中提到的:

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

还有§2.8.1:

Java虚拟机没有信号NaN值。

因此,在技术上只有一个NaN。但是§4.2.3 of the JLS也说(紧跟在您的引言后):

大多数情况下,Java SE平台将给定类型的NaN值视为已折叠成单个规范值,因此本规范通常将任意NaN称为规范值。然而,Java SE平台的1.3版本引入了一些方法,使程序员能够区分NaN值:Float.floatToRawIntBits和Double.doubleToRawLongBits方法。有兴趣的读者可以参考Float和Double类的规范以获取更多信息。
我认为这正是您和CandiedOrange提出的问题:它取决于底层处理器,但Java将它们都视为相同的。更好的是:显然,您的NaN值完全可能被静默转换为不同的NaN值,如Double.longBitsToDouble()所述:
注意,该方法可能无法返回与长参数完全相同位模式的双精度NaN。IEEE 754区分两种NaN,静默NaN和信号NaN。这两种NaN之间的差异通常在Java中不可见。对信号NaN的算术运算将其转换为具有不同但通常相似的位模式的静默NaN。然而,在某些处理器上,仅复制信号NaN也执行该转换。特别是,复制信号NaN以将其返回到调用方法可能执行此转换。因此,longBitsToDouble可能无法返回带有信号NaN位模式的双精度数。因此,对于某些长值,doubleToRawLongBits(longBitsToDouble(start))可能不等于start。此外,表示信号NaN的特定位模式取决于平台;尽管所有NaN位模式,无论是静默还是信号,都必须在上面标识的NaN范围内。
供参考,硬件相关的NaN表here。简而言之:
- 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

因此,要验证这一点,您确实需要其中一种处理器并尝试它。欢迎任何有关如何解释功率和Alpha架构的较长值的见解。


2
有数百万个有效的NaN位模式,但只有一个标准NaN。如果OP更好地询问JVM在产生NaN时是否需要按照规范始终产生标准NaN,那就更好了。看起来这并非必需,因此存在硬件依赖性。其中一个概念是“标准NaN”。 - Mishax
2
显然,根据Double.longBitsToDouble()的描述,你的NaN值可能会被静默地转换为不同的NaN值。我在我的x86 AMD CPU上实际观察到了这一点。如果我生成的浮点数位模式在7f800001到7f8036a9范围内,它们会在我查看它们之前被静默地转换为7fc00001到7fc036a9(即设置了quiet NaN位)。 - Boann
我认为Alpha的那些NaN值不正确:0x2aa8000000000000是IEEE 754中有限数字(确切地说是3.348595872897289986960303364....E-103)的表示形式,据我所知,一些Alpha机器支持的VAX G浮点格式不支持NaN。 - Mark Dickinson
1
@jmiserez +1,非常好的研究答案。它提供了完美的证据,说明为什么Java拒绝将NaN标记为信号或静默。x86和PA-RISC都使用相同的值,但赋予它们相反的含义。难怪Java举起双手说:这是一个NaN。 - candied_orange
1
@jmiserez:是的,我不确定如何读取那个表格;它对我来说没有太多意义。而且,VAX浮点数是不同的;但是它的不同之处在于它甚至没有NaN(或无穷大),因此如果该表格是指VAX浮点数,则根本没有意义使用NaN位模式。坦率地说,我认为该表格的作者很困惑。 :-) - Mark Dickinson
显示剩余3条评论

16

据我阅读JLS的理解,NaN的确切位值取决于谁/什么制造了它,由于JVM没有创建它,因此不要问他们。这就像询问“错误代码4”字符串的含义一样。

硬件生成不同的位模式以表示不同类型的NaN。不幸的是,不同种类的硬件为相同类型的NaN产生不同的位模式。幸运的是,Java可以使用标准模式来告诉我们它是某种类型的NaN。

这就像Java查看“错误代码4”字符串并说:“我们不知道您的硬件上的'code 4'是什么意思,但该字符串中有'error'这个词,所以我们认为它是一个错误。”

JLS试图给你一个自己解决问题的机会:

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

这对我来说就像一个C++的reinterpret_cast。Java给了你一个机会去分析NaN,以防你碰巧知道它的信号是如何编码的。如果你想追踪硬件规格,以便可以预测不同事件应该产生哪些NaN位模式,那么你可以自由地这么做,但你已经超出了JVM所给我们的一致性范围,因此可能会从一个硬件到另一个硬件发生变化。

在测试一个数字是否为NaN时,我们检查它是否等于它自己,因为它是唯一一个不是的数字。这并不意味着位数不同。在比较位之前,JVM检测许多位模式,以表明它是NaN。如果它是这些模式中的任何一个,则报告它不相等,即使两个操作数的位实际上是相同的(甚至如果它们不同)。

在1964年,当被要求准确定义色情时,美国最高法院法官斯图尔特曾经著名地说过:“我看到就知道它是什么”。我认为Java也像NaN一样。Java无法告诉你“信号”NaN可能在传递什么信号,因为它不知道该信号如何编码。但是它可以查看二进制位并确定它是某种NaN,因为该模式遵循一种标准。
如果你恰好使用编码所有NaN的硬件,你永远无法证明Java在进行任何使NaN具有统一二进制位的操作。同样,以我阅读JLS的方式,他们直截了当地说你自己想办法。
我能理解为什么会感觉不可靠。这确实不可靠。但这不是Java的错。我敢打赌,在那些有创意的硬件制造商中,一些人已经提出了非常富有表现力的信令NaN位模式,但他们未能使其成为足够广泛的标准,以便Java可以依靠它。这就是不可靠之处。我们拥有所有这些保留用于信号的比特位,以告诉我们NaN的种类,但我们不能使用它们,因为我们没有达成共识。让Java将NaN打成一个统一的值,这只会破坏信息,损害性能,且唯一的回报就是看起来不那么不可靠。考虑到情况,我很高兴他们意识到可以通过定义NaN不等于任何东西来欺骗方式解决问题。

3
最后一句话的意思是:Java并没有定义NaN不等于任何值,而且这也不是问题的关键点。 - user253751
4
我理解你的回答。Java不能保证NaN具有特定的位模式,因为它旨在使几乎所有程序跨平台运行,每个平台都会有自己的NaN硬件表示。 - Kevin
1
@immibis IEEE 定义,涉及 NaN 的比较将始终返回 false。Java 将 equal 定义为给出该比较。他们不必这样做。他们可以定义两个 signaling NaN 为相等。如果他们有一种可靠的方法来识别 NaN 的类型,那甚至可能会起作用。但是他们没有找到方法。其他语言试图找到解决方法:http://www.johndcook.com/blog/2009/07/21/ieee-arithmetic-python/ - candied_orange

15

这是一个演示不同NaN位模式的程序:

public class Test {
  public static void main(String[] arg) {
    double myNaN = Double.longBitsToDouble(0x7ff1234512345678L);
    System.out.println(Double.isNaN(myNaN));
    System.out.println(Long.toHexString(Double.doubleToRawLongBits(myNaN)));
    final double zeroDivNaN = 0.0 / 0.0;
    System.out.println(Double.isNaN(zeroDivNaN));
    System.out
        .println(Long.toHexString(Double.doubleToRawLongBits(zeroDivNaN)));
  }
}

输出:

true
7ff1234512345678
true
7ff8000000000000

无论硬件如何,程序本身都可以创建NaNs,这些NaNs可能与例如0.0/0.0不同,并且在程序中可能具有某种含义。


5
到目前为止,我通过正常的算术运算得到的仅有的另一个NaN值与此相同,只不过符号相反:
public static void main(String []args) {
    Double tentative1 = 0d/0d;
    Double tentative2 = Math.sqrt(-1d);
    
    System.out.println(tentative1);
    System.out.println(tentative2);
    
    System.out.println(Long.toHexString(Double.doubleToRawLongBits(tentative1)));
    System.out.println(Long.toHexString(Double.doubleToRawLongBits(tentative2)));
    
    System.out.println(tentative1 == tentative2);
    System.out.println(tentative1.equals(tentative2));
}

输出:

非数字

非数字

正无穷大

负无穷大


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