为什么改变求和顺序会返回不同的结果?
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 - 0.333333…)不能精确地表示。
为什么仅仅改变元素的顺序就会影响结果?
当前回答
为了给这里的其他答案添加一个不同的角度,这个SO答案表明,有一些方法可以进行浮点数学,其中所有求和顺序在位级上返回完全相同的值。
其他回答
我认为这与评估的顺序有关。虽然和在数学世界中自然是一样的,但在二进制世界中,它不是a + B + C = D,而是
A + B = E
E + C = D(1)
这是第二步,浮点数可以脱离。
当你改变顺序时,
A + C = F
F + B = D(2)
也许这个问题很愚蠢,但是为什么仅仅改变元素的顺序就会影响结果呢?
它将根据值的大小更改值舍入的点。作为我们正在看到的事情的一个例子,让我们假设我们使用的不是二进制浮点数,而是具有4位有效数字的小数点浮点类型,其中每个加法都以“无限”精度执行,然后舍入到最接近的可表示数字。这里有两个总和:
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
This demonstrates possibly more clearly that the important part is that we have a limited number of significant digits - not a limited number of decimal places. If we could always keep the same number of decimal places, then with addition and subtraction at least, we'd be fine (so long as the values didn't overflow). The problem is that when you get to bigger numbers, smaller information is lost - the 10001 being rounded to 10000 in this case. (This is an example of the problem that Eric Lippert noted in his answer.)
需要注意的是,右边第一行的值在所有情况下都是相同的——因此,尽管理解十进制数(23.53,5.88,17.64)不会精确地表示为双精度值很重要,但这只是因为上面所示的问题而造成的问题。
这是二进制的情况。正如我们所知,一些浮点值不能精确地用二进制表示,即使它们可以精确地用十进制表示。这三个数字只是事实的例子。
用这个程序,我输出了每个数字的十六进制表示形式和每次加法的结果。
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);
}
}
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 vs.对于第二组加法运算,4047866666666667。这个错误很大,足以显示在打印出来的总数中。
总之,在对IEEE数字执行数学运算时要小心。有些表示是不准确的,当尺度不同时,它们会变得更加不准确。如果可以,可以加减相似比例的数字。
这实际上涵盖的不仅仅是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的结果。
为了给这里的其他答案添加一个不同的角度,这个SO答案表明,有一些方法可以进行浮点数学,其中所有求和顺序在位级上返回完全相同的值。