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

时间:2014-07-31 02:56:14

标签: java floating-point nan ieee-754

我正在阅读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的特定位模式,但程序员也可以创建具有不同位模式的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之间没有差异。

我的问题是,无论编译器或虚拟机的CPU ,是否保证在Java中保证输出,或者它是否真的无法预测? JLS对此很神秘。

如果保证0.0/0.0的输出,是否有任何算术方法可以产生具有其他(可能与硬件相关的?)位模式的NaN? (我知道intBitsToFloat / longBitsToDouble可以对其他NaN进行编码,但我想知道其他值是否可以从正常算术中发生。)


后续要点:我注意到Float.NaNDouble.NaN指定了他们的确切位模式,但在源(FloatDouble)中他们是由0.0/0.0生成。如果该除法的结果实际上取决于编译器的硬件,那么在规范或实现中似乎存在缺陷。

4 个答案:

答案 0 :(得分:35)

这是§2.3.2 of the JVM 7 spec对此的评价:

  

double值集的元素正是可以表示的值   使用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()中所述:

  

请注意,此方法可能无法返回具有与long参数完全相同的位模式的双NaN。 IEEE 754区分了两种NaN,即安静的NaN和信号NaN。两种NaN之间的差异通常在Java中不可见。对信令NaN的算术运算将它们变成具有不同但通常类似的位模式的安静NaN。然而,在一些处理器上,仅复制信令NaN也执行该转换。特别地,复制信令NaN以将其返回到调用方法可以执行该转换。因此longBitsToDouble可能无法返回带有信号NaN位模式的double。因此,对于某些长值,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

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

答案 1 :(得分:14)

我在这里阅读JLS的方式,NaN的确切位值取决于是谁/是什么造成的,而且由于JVM没有做到,所以不要问他们。你也可以问他们"错误代码4"字符串意味着

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

就像Java看了"错误代码4"字符串并说,"我们不知道代码4'意味着你的硬件,但有“错误”字样。在该字符串中,我们认为这是一个错误。"

JLS试图让你有机会自己解决这个问题:

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

我认为这就像C ++ reinterpret_cast。它让你有机会自己分析NaN,以防你碰巧知道它的信号是如何编码的。如果您想要追踪硬件规格,那么您可以预测哪些不同的事件应该产生哪些NaN位模式,您可以自由地这样做,但是您不在JVM意图给我们的统一性之外。所以期望它可能会从硬件变为硬件。

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

早在1964年,美国最高法院大法官斯图尔特(Stewart)一直着名地说:“当我看到它时,我知道它”。我认为Java与NaN的做法是一样的。 Java无法告诉你任何事情"信令" NaN可能是信号,因为它不知道该信号是如何编码的。但它可以查看这些位并告诉它是某种NaN,因为该模式遵循一个标准。

如果您碰巧使用编码所有NaN编码器的硬件,那么您永远不会证明Java正在做任何事情来使NaN具有统一的位。再一次,我读JLS的方式,他们完全是说你自己在这里。

我可以看出为什么这种感觉很脆弱。它很脆弱。这不是Java的错。我想知道一些有进取心的硬件制造商在那里提出了奇妙的表达信号NaN位模式,但他们未能将其作为标准广泛采用,Java可以依靠它。那是什么片状。我们保留所有这些位用于发信号通知我们拥有哪种NaN并且不能使用它们,因为我们不同意它们的含义。让Java在硬件制造之后将NaN打成一个统一的值只会破坏这些信息,损害性能,唯一的回报就是看起来不那么脆弱。鉴于这种情况,我很高兴他们意识到他们可以欺骗他们解决问题并将NaN定义为不等于任何事情。

答案 2 :(得分:14)

这是一个演示不同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

无论硬件做什么,程序本身都可以创建可能与例如硬件不同的NaN。 0.0/0.0,可能在程序中有一些含义。

答案 3 :(得分:4)

到目前为止,我可以使用常规算术运算生成的唯一其他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));
}

输出:

  

的NaN

     

的NaN

     

7ff8000000000000

     

fff8000000000000