关于java:为什么更改sum顺序会返回不同的结果?

Why does changing the sum order returns a different result?

为什么更改和顺序会返回不同的结果?

23.53 + 5.88 + 17.64=47.05

23.53 + 17.64 + 5.88=47.050000000000004

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)不会精确表示为double值,但这只是一个问题,因为上面显示的问题。


这是二进制的情况。如我们所知,有些浮点值不能精确地用二进制表示,即使它们可以精确地用十进制表示。这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);
   }
}

printValueAndInHex方法只是一个十六进制打印机助手。

输出如下:

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个数字是xyzs的十六进制表示。在IEEE浮点表示法中,位2-12表示二进制指数,即数字的刻度。(第一位是符号位,尾数的剩余位。)表示的指数实际上是二进制数减去1023。

提取前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

第一组附加项

第二个数字(y)的大小较小。当把这两个数相加得到x + y时,第二个数的最后2位(01位)被移出范围,不计入计算。

第二个加法加上x + yz,并加上两个相同比例的数字。

第二组加法

这里,x + z首先出现。它们具有相同的规模,但它们产生的数量在规模上更高:

1
404 => 0|100 0000 0100| => 1028 - 1023 = 5

第二个加法加上x + zy,现在从y中减去3位来相加(101)。这里必须向上取整,因为结果是下一个浮点数向上:第一组加法为4047866666666666,第二组加法为4047866666666667。这个错误非常严重,足以在打印出的总数中显示出来。

总之,在对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)