为何Java浮点数加法表现出尾数为24位精度的特性?
引言
在Java浮点数运算中,我发现给某一范围的极小负数加上1.0时,结果仍为1.0。为理解这一现象,我深入研究了浮点数的工作原理,但遇到了困惑。浮点数的二进制表示能让运算逻辑更清晰,因此本文将基于二进制展开分析。
简而言之,从运算结果和代码输出来看,浮点数在加减运算时的尾数精度似乎是24位(不含隐含的前导1),而非预期的23位。
当我们将0b1_01100110_00000000000000000000000(即-1×2-25×1.0)与`0b0_01111111_00000000000000000000000`(即1×20×1.0,也就是浮点数1.0的二进制表示)相加时,结果为1.0。前者就是那个会导致此特殊结果的负数,而比它略小的数(通过Math.nextDown()得到的0b1_01100110_00000000000000000000001)则不会出现这种情况。从该负数到-0.0f的所有数都有此特性。
运算分析
对于这个特殊数0b1_01100110_00000000000000000000000,它的指数是-25,小于1.0的指数0,因此需要将其指数加25以匹配1.0的指数,同时将尾数右移25位。最终指数变为01111111,尾数变为0.00000000000000000000000[01]。隐含的1右移25位后变为0,由于尾数只能保留23位,方括号内的部分会丢失,因此新尾数被截断为0.00000000000000000000000。此时用1.0的尾数1.00000000000000000000000减去该数的新尾数,结果仍是1.0的尾数(1-0=1),组合后得到0b0_01111111_00000000000000000000000,即1.0。
这种因精度限制导致的信息丢失解释了为何该特殊数被当作0处理,但另一个数的运算结果却出现了异常。
再看0b1_01100111_00000000000000000000000(即-1×2^-24×1.0),它与前者类似,只是指数为-24。与1.0相加时,同样将指数加24以匹配1.0的指数,得到01111111,同时将尾数右移24位,得到0.00000000000000000000000[1]。按预期,由于已有23个0,末尾的1应该被截断,但实际运行代码后,结果却并非如此。
若不截断尾数进行运算:1.00000000000000000000000[0] - 0.00000000000000000000000[1] = 0.11111111111111111111[1]。由于浮点数需要隐含前导1,需将所有位左移1位,得到1.11111111111111111111[0],同时将指数减1,得到01111110(即-1),完成归一化。最终结果为0b0_01111110_11111111111111111111111,与代码输出完全一致。
问题
既然浮点数的尾数通常用23位表示,为何代码运行时表现出尾数为24位的特性?
辅助可视化代码
// to visualize the float bits private String floatToBinaryString(float value) { String binaryString = String.format("%32s", Integer.toBinaryString(Float.floatToIntBits(value))).replace(' ', '0'); return "0b" + binaryString.charAt(0) + "_" + binaryString.substring(1, 9) + "_" + binaryString.substring(9); } // The 2^-25 number + 1.0 outputs 0b0_01111111_00000000000000000000000 or 1.0 System.out.println(floatToBinaryString( Float.intBitsToFloat(0b1_01100110_00000000000000000000000) + Float.intBitsToFloat(0b0_01111111_00000000000000000000000))); // The 2^-24 number + 1.0 outputs 0b0_01111110_11111111111111111111111 or 0.99999994 System.out.println(floatToBinaryString( Float.intBitsToFloat(0b1_01100111_00000000000000000000000) + Float.intBitsToFloat(0b0_01111111_00000000000000000000000)));
内容的提问来源于stack exchange,提问作者PianoMastR64

