NaN的位模式是否真的依赖于硬件?
我正在阅读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返回一个特定的位模式,程序员也可以创build具有不同位模式的NaN,以编码例如回顾性诊断信息。
JLS中的文本似乎意味着例如0.0/0.0
的结果具有依赖于硬件的位模式,并且取决于该expression式是否被计算为编译时间常数,它所依赖的硬件可能是Java程序编译的硬件或程序运行的硬件。 如果这是真的,这一切似乎非常片断。
我跑了下面的testing:
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指定了它们的确切位模式,但是在源代码( 浮点 , 双 精度 )中,它们是由0.0/0.0
生成的。 如果这种划分的结果真的取决于编译器的硬件,那么规范或实现似乎就有缺陷。
这是JVM 7规范的§2.3.2所要说的:
除了只有一个NaN值(IEEE 754指定了2 53 -2个不同的NaN值)之外,double值集合的元素正好是可以使用IEEE 754标准中定义的双重浮点格式表示的值。
和§2.8.1 :
Java虚拟机没有信号NaN值。
所以技术上只有一个NaN。 但是JLS的§4.2.3也是这样说的(在引用之后):
大多数情况下,Java SE平台将给定types的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位模式的双精度型。 因此,对于一些long值,doubleToRawLongBits(longBitsToDouble(start))可能不等于start。 此外,哪些特定的位模式表示信令NaN是平台相关的; 尽pipe所有的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
所以,要validation这一点,你真的需要这些处理器之一,并去尝试一下。 对于如何解释Power和Alpha体系结构的更长的值,任何见解都是值得欢迎的。
这是一个演示不同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
不pipe硬件是什么,程序本身都可以创buildNaN,可能与0.0/0.0
不一样,并且在程序中可能有一些含义。
我在这里阅读JLS的方式是,NaN的确切位值取决于是谁制作的,因为JVM没有制作,所以不要问他们。 你可能会问他们什么是“错误代码4”string的意思。
硬件产生不同的位模式,以表示不同种类的NaN。 不幸的是,不同types的硬件为相同types的NaN产生不同的位模式。 幸运的是,Java可以使用的标准模式至less可以说明它是某种NaN。
这就像Java看“错误代码4”string,并说:“我们不知道你的硬件上代码4是什么意思,但是那个string中有'错误'这个词,所以我们认为这是一个错误。 “
JLS试图让你有机会自己弄清楚:
“然而,Java SE平台1.3版引入了一些方法,使程序员能够区分NaN值: Float.floatToRawIntBits
和Double.doubleToRawLongBits
方法,感兴趣的读者可以参考Float
和Double
类的规范了解更多信息。
这看起来像一个C ++ reinterpret_cast
。 如果您碰巧知道信号是如何编码的,那么Java就会给您一个分析NaN的机会。 如果你想跟踪硬件规格,那么你可以预测哪些不同的事件应该产生哪些NaN位模式,你可以自由地这样做,但是你超出了JVM本来想给我们的一致性。 所以期望它可能会从硬件改变到硬件。
当testing一个数字是否为NaN时,我们检查它是否与自己相等,因为它是唯一不是的数字。 这并不是说这些位是不同的。 在比较这些比特之前,JVMtesting了许多称为NaN的比特模式。 如果是这些模式中的任何一种,那么即使两个操作数的位相同(即使它们不相同),也报告它不相等。
早在1964年,当被迫准确定义色情时,美国最高法院法官斯图尔特(Stewart Stewart)就曾说过:“当我看到它时,我就知道了”。 我认为Java与NaN做同样的事情。 Java不能告诉你任何“信号”NaN可能是信号,因为它不知道信号是如何编码的。 但是它可以看到这些位,并且告诉它这是一种NaN,因为这个模式遵循一个标准。
如果你碰巧在硬件上编码所有NaN的统一位,你永远不会certificateJava正在做任何事情来使NaN具有统一的位。 再次,我阅读JLS的方式,他们直接说你在这里。
我可以看到为什么这感觉片状。 这是片状的。 这只是Java的错。 我会说,一些有进取的硬件制造商在那里有一些奇妙的expression信号NaN位模式,但是他们没有把它作为一个标准被广泛采用,以至于Java可以依靠它。 这就是片状。 我们把所有这些位保留下来,用来表示我们有什么样的NaN,不能使用它们,因为我们不同意它们的含义。 让Java把NaN打成一个统一的价值后,硬件使得它们只会破坏这些信息,损害性能,唯一的回报就是看起来不是片面的。 考虑到这种情况,我很高兴他们意识到他们可以通过欺骗手段解决问题,并将NaN定义为与任何事物不相等。
到目前为止,通过正常的算术运算可以生成的唯一的其他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
假
真正