为什么更改求和顺序会返回不同的结果?
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...)无法精确表示。
为什么简单地改变元素的顺序会影响结果?
也许这个问题很愚蠢,但为什么简单地改变元素的顺序会影响结果?
它将根据值的大小更改四舍五入的点。作为我们所看到的那种事情的一个例子,让我们假设我们使用的是具有 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
值很重要,但这很重要只是因为上面显示的问题而导致的问题。
这是二进制文件中发生的事情。众所周知,一些浮点值不能用二进制精确表示,即使它们可以用十进制精确表示。这三个数字只是这个事实的例子。
通过这个程序,我输出每个数字的十六进制表示和每个加法的结果。
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
,现在去掉3位y
来加上数字 ( 101
)。在这里,必须向上舍入,因为结果是下一个浮点数向上:4047866666666666
第一组加法与4047866666666667
第二组加法。该错误足以显示在总数的打印输出中。
总之,对 IEEE 数字执行数学运算时要小心。有些表示是不精确的,当尺度不同时,它们变得更加不精确。如果可以,添加和减去类似比例的数字。
乔恩的回答当然是正确的。在您的情况下,误差不大于您在执行任何简单浮点运算时累积的误差。你有一个场景,在一种情况下你得到零错误,而在另一种情况下你得到一个小错误;这实际上并不是一个有趣的场景。一个很好的问题是:是否存在将计算顺序从微小错误变为(相对)巨大错误的场景?答案毫无疑问是肯定的。
考虑例如:
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 的值相差很大是很有趣的。看看你能不能做到!
这实际上涵盖的不仅仅是 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 的结果。
浮点数使用 IEEE 754 格式表示,该格式为尾数(有效数)提供特定大小的位。不幸的是,这为您提供了特定数量的“分数积木”,并且某些分数无法精确表示。
在您的情况下发生的情况是,在第二种情况下,由于评估添加的顺序,添加可能会遇到一些精度问题。我还没有计算这些值,但可能是例如 23.53 + 17.64 无法精确表示,而 23.53 + 5.88 可以。
不幸的是,这是一个您必须处理的已知问题。