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

Boa*_*ann 55 java floating-point nan ieee-754

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

seee eeee emmm mmmm mmmm mmmm mmmm mmmm
Run Code Online (Sandbox Code Playgroud)

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)));
Run Code Online (Sandbox Code Playgroud)

我机器上的输出是:

7fc00000
7fc00000
7ff8000000000000
7ff8000000000000
Run Code Online (Sandbox Code Playgroud)

输出显示没有超出预期.指数位都是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指定了它们的确切位模式,但是在源(Float,Double)中它们是由它们生成的0.0/0.0.如果该划分的结果实际上取决于编译器的硬件,那么在规范或实现中似乎存在缺陷.

jmi*_*rez 36

这就是JVM 7规范的§2.3.2所说的:

双值集的元素恰好是可以使用IEEE 754标准中定义的双浮点格式表示的值,除了只有一个NaN值(IEEE 754指定2 53 -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值完全有可能被静默地转换为不同的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的表在这里.综上所述:

- 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
Run Code Online (Sandbox Code Playgroud)

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

  • +1 提及“规范 NaN”概念。有数百万个有效的 NaN 位模式,但只有一个规范的 NaN。OP 可以更好地询问 JVM 是否需要其规范在生成 NaN 时始终生成规范的 NaN。似乎这不是必需的,因此存在硬件依赖性的空间。 (2认同)
  • "显然,完全有可能你的NaN值被静默地转换为不同的NaN,如Double.longBitsToDouble()中所述"我已经在我的x86 AMD CPU上观察到了这一点.如果我在7f800001到7f8036a9范围内生成具有位模式的浮点数,它们会在我有机会查看它们之前以静默方式转换为7fc00001到7fc036a9(即它设置安静的NaN位). (2认同)

can*_*nge 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.这是Java让你有机会自己分析NaN,以防你碰巧知道它的信号是如何编码的.如果您想要追踪硬件规格,那么您可以预测哪些不同的事件应该产生哪些NaN位模式,您可以自由地这样做,但是您不在JVM意图给我们的统一性之外.所以期望它可能会从硬件变为硬件.

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

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

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

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

  • +1这个答案对我来说很有意义.Java不保证NaN将具有特定的位模式,因为它旨在使几乎任何程序跨平台,并且每个平台都将拥有自己的NaN硬件表示. (4认同)
  • 最后一句:不是Java将NaN定义为不等于任何东西,而这不是问题所在的...... (3认同)

Pat*_*han 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)));
  }
}
Run Code Online (Sandbox Code Playgroud)

输出:

true
7ff1234512345678
true
7ff8000000000000
Run Code Online (Sandbox Code Playgroud)

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


fal*_*lla 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));
}
Run Code Online (Sandbox Code Playgroud)

输出:

7ff8000000000000

fff8000000000000

错误的

真的