为什么改变求和顺序会返回不同的结果?
__abc0 __abc2 __abc1
Java和JavaScript返回相同的结果。
我理解,由于浮点数用二进制表示的方式,一些有理数(比如1/3 - 0.333333…)不能精确地表示。
为什么仅仅改变元素的顺序就会影响结果?
我认为这与评估的顺序有关。虽然和在数学世界中自然是一样的,但在二进制世界中,它不是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
这可能更清楚地表明,重要的部分是我们有有限数量的有效数字 -而不是有限数量的小数点后。如果我们能始终保持小数点后的相同位数,那么至少加减法是可以的(只要值不溢出)。问题是,当你得到更大的数字时,更小的信息会丢失——在这种情况下,10001被四舍五入为10000。(这是一个问题的例子,Eric Lippert在他的回答中指出.)
需要注意的是,右边第一行的值在所有情况下都是相同的——因此,尽管理解十进制数(23.53,5.88,17.64)不会精确地表示为double值很重要,但这只是一个问题,因为上面显示的问题。
double
浮点数使用IEEE 754格式表示,该格式为尾数(有效位)提供了特定的位大小。不幸的是,这给了你一个特定数量的“分数构建块”来玩,某些分数值不能精确地表示。
在您的情况下发生的情况是,在第二种情况下,由于计算加法的顺序,加法可能会遇到一些精度问题。我没有计算过具体的数值,可能是23.53 + 17.64不能精确表示,而23.53 + 5.88可以。
不幸的是,这是一个已知的问题,你必须处理。
这实际上涵盖的不仅仅是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的结果。
这是二进制的情况。正如我们所知,一些浮点值不能精确地用二进制表示,即使它们可以精确地用十进制表示。这三个数字只是事实的例子。
用这个程序,我输出了每个数字的十六进制表示形式和每次加法的结果。
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方法只是一个十六进制打印机助手。
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。
x
y
z
s
提取前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
01
第二次添加x + y和z,并添加两个相同规模的数字。
第二组补充
这里,x + z首先出现。它们具有相同的规模,但它们产生的数字在规模上更高:
x + z
404 => 0|100 0000 0100| => 1028 - 1023 = 5
第二次添加添加了x + z和y,现在3.位从y删除,以添加数字(101)。这里,必须有一个向上的整数,因为结果是下一个向上的浮点数:4047866666666666用于第一组加法运算,4047866666666667用于第二组加法运算。这个错误很大,足以显示在打印出来的总数中。
101
4047866666666666
4047866666666667
总之,在对IEEE数字执行数学运算时要小心。有些表示是不准确的,当尺度不同时,它们会变得更加不准确。如果可以,可以加减相似比例的数字。
乔恩的回答当然是正确的。在你的例子中,误差不会比你做任何简单浮点运算所积累的误差大。在这种情况下,一种情况下误差为零而另一种情况下误差很小;这其实不是一个有趣的场景。一个很好的问题是:
举个例子:
x1 = (a - b) + (c - d) + (e - f) + (g - h);
vs
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的值相差很大,这是很有趣的。看看你能不能做到!
为了给这里的其他答案添加一个不同的角度,这个SO答案表明有一些方法可以执行浮点数学,其中所有求和顺序在位级上返回完全相同的值。