23.53 + 5.88 + 17.64
= 47.05
23.53 + 17.64 + 5.88
= 47.050000000000004
无论是Java还是JavaScript都会返回相同的结果。我了解由于浮点数在二进制中的表示方式,一些有理数(如1/3 - 0.333333...)不能精确地表示。
为什么只是改变元素的顺序就会影响结果呢?
23.53 + 5.88 + 17.64
= 47.05
23.53 + 17.64 + 5.88
= 47.050000000000004
无论是Java还是JavaScript都会返回相同的结果。1/3 + 2/3 + 2/3 = (0.3333 + 0.6667) + 0.6667
= 1.000 + 0.6667 (no rounding needed!)
= 1.667 (where 1.6667 is rounded to 1.667)
2/3 + 2/3 + 1/3 = (0.6667 + 0.6667) + 0.3333
= 1.333 + 0.3333 (where 1.3334 is rounded to 1.333)
= 1.666 (where 1.6663 is rounded to 1.666)
10000 + 1 - 10000 = (10000 + 1) - 10000
= 10000 - 10000 (where 10001 is rounded to 10000)
= 0
10000 - 10000 + 1 = (10000 - 10000) + 1
= 0 + 1
= 1
double
值,但这只是因为上述问题所示的问题。以下是二进制中正在发生的情况。我们知道,一些浮点数在二进制下无法准确表示,即使它们在十进制下可以准确表示。这三个数字只是这个事实的例子。
通过这个程序,我输出了每个数字的十六进制表示以及每个加法的结果。
public class Main{
public static void main(String args[]) {
double x = 23.53; // Inexact representation
double y = 5.88; // Inexact representation
double z = 17.64; // Inexact representation
double s = 47.05; // What math tells us the sum should be; still inexact
printValueAndInHex(x);
printValueAndInHex(y);
printValueAndInHex(z);
printValueAndInHex(s);
System.out.println("--------");
double t1 = x + y;
printValueAndInHex(t1);
t1 = t1 + z;
printValueAndInHex(t1);
System.out.println("--------");
double t2 = x + z;
printValueAndInHex(t2);
t2 = t2 + y;
printValueAndInHex(t2);
}
private static void printValueAndInHex(double d)
{
System.out.println(Long.toHexString(Double.doubleToLongBits(d)) + ": " + d);
}
}
< p > printValueAndInHex
方法只是一个帮助程序员打印十六进制的辅助工具。
输出如下:
403787ae147ae148: 23.53
4017851eb851eb85: 5.88
4031a3d70a3d70a4: 17.64
4047866666666666: 47.05
--------
403d68f5c28f5c29: 29.41
4047866666666666: 47.05
--------
404495c28f5c28f6: 41.17
4047866666666667: 47.050000000000004
前4个数字是x
, y
, z
, 和 s
的十六进制表示。在IEEE浮点表示中,位2-12代表二进制的指数,即该数值的比例尺。 (第一位是符号位,其余位用于小数部分。)实际上表示的指数是二进制数减去1023。
前4个数字的指数已被提取:
sign|exponent
403 => 0|100 0000 0011| => 1027 - 1023 = 4
401 => 0|100 0000 0001| => 1025 - 1023 = 2
403 => 0|100 0000 0011| => 1027 - 1023 = 4
404 => 0|100 0000 0100| => 1028 - 1023 = 5
第一组加法
第二个数字(y)的幅度较小。当将这两个数字相加得到(x+y)时,第二个数字(01)的最后2位被移出范围,不参与计算。
第二次加法是将(x+y)和z相加,并添加两个相同幅度的数字。
第二组加法
这里,首先进行的是(x+z)的相加。它们具有相同的幅度,但产生的结果在更高的幅度上:
404 => 0|100 0000 0100| => 1028 - 1023 = 5
第二次修改添加了x+z
和y
,现在从y
中减去3位来加上数字(101
)。在这里,必须向上舍入,因为结果是下一个浮点数的上限:4047866666666666
对于第一组加法,而4047866666666667
对于第二组加法。这个误差足以在总输出中显示。=)
,你的十六进制打印机助手真的很棒! - ADTC乔恩的回答当然是正确的。在您的情况下,错误不会比执行任何简单浮点运算积累的误差更大。您面临的情况是,在某些情况下,您获得零误差,而在另一种情况下,您获得微小误差;实际上这并不是一个非常有趣的情况。一个好问题是:是否存在一些情况,其中改变计算顺序会从微小误差变为(相对)巨大的误差? 答案毫无疑问是肯定的。
例如考虑:
x1 = (a - b) + (c - d) + (e - f) + (g - h);
对抗
x2 = (a + c + e + g) - (b + d + f + h);
对阵
x3 = a - b + c - d + e - f + g - h;
显然,在精确计算中它们是相同的。尝试找到a,b,c,d,e,f,g,h的值,使得x1、x2和x3的值之间有很大的差异,这很有趣。看看你能否做到!
double d = double.MaxValue; Console.WriteLine(d + d - d - d); Console.WriteLine(d - d + d - d);
- 输出结果为Infinity和0。 - Jon Skeet这实际上涵盖了不仅仅是Java和Javascript,而且可能会影响使用浮点数或双精度浮点数的任何编程语言。
在内存中,浮点数使用类似于IEEE 754的特殊格式(转换器提供了比我更好的解释)。
无论如何,这是一个浮点数转换器。
http://www.h-schmidt.net/FloatConverter/
关于操作顺序的问题是操作的“精细程度”。
您的第一行从前两个值中得出29.41,这使我们的指数为2 ^ 4。
您的第二行产生41.17,这使我们的指数为2 ^ 5。
通过增加指数,我们失去了一个有效数字,这很可能会改变结果。
尝试在41.17上打开和关闭最右边的最后一位,您可以看到像1/2 ^ 23这样“微不足道”的东西足以引起这种浮点差异。
编辑:对于那些记得有效数字的人来说,这将属于该类别。10 ^ 4 + 4999的有效数字为1,将成为10 ^ 4。在这种情况下,有效数字要小得多,但我们可以看到附加的.00000000004带来的结果。
我认为这与计算顺序有关。虽然在数学世界中,总和自然相同,但在二进制世界中,A + B + C = D 被转换为
A + B = E
E + C = D(1)
因此,有一个次要步骤,浮点数可能会出现偏差。
当您更改顺序时,
A + C = F
F + B = D(2)
>>> 0.1 + 0.2 + 0.3
0.6000000000000001
>>> 0.2 + 0.3 + 0.1
0.6
这归结于你舍入的位置。使用Decimal
包时也会遇到同样的问题。以下是一个Python示例:
>>> from decimal import Decimal, get_context
>>> getcontext().prec = 1
>>> Decimal("0.11") + Decimal("0.23") + Decimal("0.33")
Decimal('0.6')
>>> Decimal("0.11") + (Decimal("0.23") + Decimal("0.33"))
Decimal('0.7')
(2.0^53 + 1) - 1 == 2.0^53 - 1 != 2^53 == 2^53 + (1 - 1)
)。因此,在选择求和和其他操作的顺序时要小心。一些编程语言提供了内置函数来执行“高精度”求和运算(例如Python中的math.fsum
),因此您可以考虑使用这些函数而不是朴素的求和算法。 - Bakuriu