对于同样的浮点数运算为何 Java 与 C 的结果不相同?

java和c同时计算0.01d+0.05d,在保留小数点后18位相同(java默认输出18位),为0.060000000000000005 疑问一:为什么末尾多了个5? 0.01d+0.05d结果如果都保留19+以上的小数位,结果就不同了,以19位为例,java: 0.0600000000000000050,c: 0.0600000000000000047 疑问二:为什么两者同是采用IEEE754标准,会出现不一样的结果,而且计算的话,也是交给CPU的,跟语言有关吗,哪里不同
关注者
147
被浏览
7648

6 个回答

@bombless 在问题的评论里写得没错。IEEE 754最重要的(大家基本上遵守的)是数据的格式。虽然也有算法上的指引(例如有各种rounding mode),但实际上大家实现得不一定那么严格。

C/C++和Java在浮点数运算上规定都比较松。

Java语言上有“FP-strict”的概念。所有浮点数常量表达式和带有strictfp修饰的代码是FP-strict的,而其它浮点数运算默认不是FP-strict的。规定如下:

Java Language Specification, Chapter 15. Expressions, 15.4. FP-strict Expressions
Within an FP-strict expression, all intermediate values must be elements of the float value set or the double value set, implying that the results of all FP-strict expressions must be those predicted by IEEE 754 arithmetic on operands represented using single and double formats.

Within an expression that is not FP-strict, some leeway is granted for an implementation to use an extended exponent range to represent intermediate results; the net effect, roughly speaking, is that a calculation might produce "the correct answer" in situations where exclusive use of the float value set or double value set might result in overflow or underflow.
我加黑的部分说的就是默认的、非FP-strict时的运算规定。可见其并不要求严格遵循IEEE 754对单精度和双精度的运算规定。现实来说就是为了容许像x86/x87这样的组合带来的中间运算结果的精度比IEEE 754所规定的高的情况。

C11对浮点类型的规定如下:

The C floating types match the IEC 60559 formats as follows:
— The float type matches the IEC 60559 single format.
— The double type matches the IEC 60559 double format.
— The long double type matches an IEC 60559 extended format,357) else a non-IEC 60559 extended format, else the IEC 60559 double format.
Any non-IEC 60559 extended format used for the long double type shall have more precision than IEC 60559 double and at least the range of IEC 60559 double.
(IEC 60559 等价于 IEEE 754)
但实际的C语言实现通常会允许程序员用精度换速度。例如GCC就有很多参数可以指定各种规则要不要遵守:gcc.gnu.org/wiki/Floati

然后C++嗯,以MSVC为例,它也允许程序员指定运算精度与速度间的取舍:Microsoft Visual C++ Floating-Point Optimization

在x86/x87环境中进行浮点数运算,如果在x87用默认精度(扩展双精度,也就是80-bit的浮点数格式)上连续做超过1次运算而不对中间结果做调整,那它的精度就会比IEEE 754的双精度要高,运算结果自然就不完全一样了。这只是一个例子,能让浮点数运算与“预期”不一样的因素挺多,例如换换顺序、折叠一下常量啥的…

我们在实现HotSpot JVM的时候在非FP-strict的地方基本上只要保证解释器跟JIT编译器对一些特定Math浮点数方法返回的结果足够接近就好了…

-----------------------

来再geek一点吧。

Java语言规范说浮点数字面量转换到浮点类型数值的算法由 java.lang.Double.valueOf(String) 方法的文档规定。而后者指定使用IEEE 754的round-to-nearest模式来转换。
Java里,
0.01d + 0.05d
是一个常量表达式,由语言规范规定必须做常量折叠。这个常量折叠是FP-strict的。

根据规范,把字面量转换为Java认知的double,

0.01d:
十六进制:
3f847ae147ae147b
二进制:
0 01111111000 0100011110101110000101000111101011100001010001111011
十进制:
0.01000000000000000020816681711721685132943093776702880859375

0.05d:
十六进制:
3fa999999999999a
二进制:
0 01111111010 1001100110011001100110011001100110011001100110011010
十进制:
0.05000000000000000277555756156289135105907917022705078125

(懒人提示:要看一个Java double的准确十进制表示,只要
new BigDecimal(x).toString()
就好了…)

可见这两个数字在double格式都无法准确表示,而只能用近似值。
这两个浮点数相加的结果是:
二进制:
0 01111111010 1110101110000101000111101011100001010001111010111001
十进制:
0.060000000000000004718447854656915296800434589385986328125

然后Java的formatter(java.text.DecimalFormat)在把这个double转换为字符串的时候,它并不追求把数字的真实值准确转换出来,而是更追求转换速度,所以会看情况round一下…所以楼主会看到类似 6.0000000000000005E-2 这样的字符串表示。

题主可以试试看不写0.01d+0.05d,而直接写0.06d然后试试把它输出成字符串看看。你会看到它其实也不是精确值,而是
0.059999999999999997779553950749686919152736663818359375
,而Java的DecimalFormat即便用
"0.0000000000000000000000000000000000000000000000E0"
的格式也会把它格式化成
6.0000000000000000000000000000000000000000000000E-2
嗯挺迷惑人的。

而楼主用的C的环境多半在格式化字符串的时候用了别的取舍所以多看到了一位…
把浮点数的具体16进制表示贴出来,用 Double.doubleToRawLongBits 。