我正在阅读Java语言规范中的浮点NaN值(我很无聊)。 32位float
具有此位格式:
seee eeee emmm mmmm mmmm mmmm mmmm mmmm
s
是符号位,e
是指数位,m
是尾数位。 NaN值被编码为所有1的指数,并且尾数位不是全0(其将是+/-无穷大)。这意味着有许多不同的可能NaN值(具有不同的s
和m
位值。)
关于这一点,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.NaN和Double.NaN指定了他们的确切位模式,但在源(Float,Double)中他们是由0.0/0.0
生成。如果该除法的结果实际上取决于编译器的硬件,那么在规范或实现中似乎存在缺陷。
答案 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.floatToRawIntBits
和Double.doubleToRawLongBits
方法。感兴趣的读者可以参考Float
和Double
类的规范以获取更多信息。"
我认为这就像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
假
真