Why does changing the sum order returns a different result?
为什么更改和顺序会返回不同的结果?
Java和JavaScript都返回相同的结果。
我理解,由于浮点数是以二进制表示的,一些有理数(如1/3-0.333333…)不能精确表示。
为什么简单地改变元素的顺序会影响结果?
Maybe this question is stupid, but why does simply changing the order of the elements affects the result?
号
它将根据数值的大小改变数值取整的点。作为我们正在看到的这类事情的一个例子,让我们假设,我们使用的不是二进制浮点,而是具有4个有效数字的十进制浮点类型,其中每个加法都以"无限"精度执行,然后四舍五入到最近的可表示数字。以下是两个总数:
1 2 3 4 5 6 7 | 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) |
我们甚至不需要非整数来解决这个问题:
1 2 3 4 5 6 7 | 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。(这是埃里克·利珀特在回答中提到的问题的一个例子。)
重要的是要注意,在所有情况下,右侧第一行的值都是相同的-因此,尽管重要的是要了解十进制数(23.53、5.88、17.64)不会精确表示为
这是二进制的情况。如我们所知,有些浮点值不能精确地用二进制表示,即使它们可以精确地用十进制表示。这3个数字就是这个事实的例子。
使用这个程序,我输出每个数字的十六进制表示和每个加法的结果。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | 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); } } |
输出如下:
1 2 3 4 5 6 7 8 9 10 | 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个数字是
提取前4个数字的指数:
1 2 3 4 5 | 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 |
第一组附加项
第二个数字(
第二个加法加上
第二组加法
这里,
1 | 404 => 0|100 0000 0100| => 1028 - 1023 = 5 |
。
第二个加法加上
总之,在对IEEE数字执行数学运算时要小心。有些表述是不准确的,当尺度不同时,它们变得更加不准确。如果可以的话,加上和减去相似比例的数字。
乔恩的回答当然是正确的。在您的情况下,错误不大于您在执行任何简单浮点操作时累积的错误。你有一个场景,在一种情况下,你得到零错误,在另一种情况下,你得到一个微小的错误;这实际上不是一个有趣的场景。一个很好的问题是:是否存在这样的情况:计算顺序从一个微小的错误变为一个(相对的)巨大的错误?答案显然是肯定的。
例如考虑:
1 | x1 = (a - b) + (c - d) + (e - f) + (g - h); |
VS
1 | x2 = (a + c + e + g) - (b + d + f + h); |
。
VS
1 | x3 = a - b + c - d + e - f + g - h; |
。
显然,在精确的算术中,它们是相同的。尝试找出a、b、c、d、e、f、g、h的值是很有趣的,这样x1、x2和x3的值就会有很大的差异。看看你能不能这样做!
这实际上涵盖的不仅仅是Java和JavaScript,而且可能会影响使用浮点或双倍的任何编程语言。
在内存中,浮点数使用了一种特殊的格式,遵循IEEE754(转换器比我能提供更好的解释)。
无论如何,这里是浮动转换器。
http://www.h-schmidt.net/floatconverter/
有关操作顺序的问题是操作的"精细度"。
您的第一行从前两个值得到29.41,这给我们2^4作为指数。
你的第二行得到41.17,这给我们2^5的指数。
我们通过增加指数失去了一个重要的数字,这可能会改变结果。
试着在最右边的最后一个位上勾选41.17,你可以看到指数的1/2^23这样的"无关紧要"的东西足以引起这个浮点差。
编辑:对于那些记住重要数字的人来说,这属于这个范畴。有效数字为1的10^4+4999将是10^4。在这种情况下,有效数字要小得多,但是我们可以看到附加了.00000000004的结果。
浮点数使用IEEE754格式表示,该格式为尾数(有效位)提供特定大小的位。不幸的是,这为您提供了一个特定数量的"分数构建块",并且某些分数值无法精确表示。
在您的案例中发生的情况是,在第二个案例中,由于评估添加的顺序,添加可能会遇到一些精度问题。我还没有计算这些值,但可以是23.53+17.64不能精确表示,而23.53+5.88可以。
不幸的是,这是一个你只需处理的已知问题。
我认为这与疏散的顺序有关。虽然在数学世界中求和自然是一样的,但在二进制世界中不是a+b+c=d,而是
1 2 | A + B = E E + C = D(1) |
所以有第二步,浮点数可以离开。
当你改变订单时,
1 2 | A + C = F F + B = D(2) |
号