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

Mar*_*des 294 javascript java floating-point

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

23.53 + 5.88 + 17.64 = 47.05

23.53 + 17.64 + 5.88 = 47.050000000000004

双方的JavaJavaScript的返回相同的结果.

我理解,由于浮点数用二进制表示的方式,一些有理数(如1/3 - 0.333333 ......)无法精确表示.

为什么简单地改变元素的顺序会影响结果?

Jon*_*eet 276

也许这个问题很愚蠢,但为什么简单地改变元素的顺序会影响结果呢?

它将根据其大小更改值舍入的点.作为我们所看到的那种事物的一个例子,让我们假设我们使用的是带有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)
Run Code Online (Sandbox Code Playgroud)

我们甚至不需要非整数来解决这个问题:

10000 + 1 - 10000 = (10000 + 1) - 10000
                  = 10000 - 10000 (where 10001 is rounded to 10000)
                  = 0

10000 - 10000 + 1 = (10000 - 10000) + 1
                  = 0 + 1
                  = 1
Run Code Online (Sandbox Code Playgroud)

这可能更清楚地表明,重要的部分是我们有限数量的有效数字 - 不是有限的小数位数.如果我们总能保持相同的小数位数,那么至少加上和减法,我们就没事了(只要值没有溢出).问题是当你得到更大的数字时,更小的信息会丢失 - 在这种情况下,10001被舍入到10000.(这是Eric Lippert在答案中提到的问题的一个例子.)

需要注意的是右手边的第一行的值在所有情况下,同样是很重要的-所以,虽然明白,你的十进制数(23.53,5.88,17.64)不会被精确表示,因为它是重要的double价值,这是由于上面显示的问题,只是一个问题.

  • "可能会延迟这个 - 现在没时间了!"急切地等待它@Jon (10认同)
  • 当我说我稍后会回到答案时,社区对我不那么友善了<在这里输入某种轻松的表情来表明我在开玩笑而不是一个混蛋> ......稍后会回到这里. (3认同)
  • @meteors:不,它不会导致溢出 - 你使用的是错误的数字.将10001舍入为10000,而不是1001舍入为1000.为了使其更清晰,54321将四舍五入为54320 - 因为它只有四位有效数字."四位有效数字"和"最大值9999"之间存在很大差异.正如我之前所说,你基本上代表x.xxx*10 ^ n,其中10000,x.xxx是1.000,n是4.这就像`double`和`float`,其中非常大数字,连续可表示的数字相差超过1. (3认同)
  • @ZongZhengLi:虽然理解这一点当然很重要,但这不是本案的根本原因.您可以编写一个类似的示例,其值**以二进制表示,并且看到相同的效果.这里的问题是同时保持大规模信息和小规模信息. (2认同)
  • @GradyPlayer:谢谢你提醒我.完成! (2认同)

rge*_*man 52

这是二进制文件中发生的事情.众所周知,某些浮点值无法用二进制精确表示,即使它们可以用十进制精确表示.这3个数字只是这个事实的例子.

使用此程序,我输出每个数字的十六进制表示和每个加法的结果.

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);
   }
}
Run Code Online (Sandbox Code Playgroud)

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
Run Code Online (Sandbox Code Playgroud)

第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
Run Code Online (Sandbox Code Playgroud)

第一组添加

第二个数字(y)的幅度较小.当添加这两个数字时x + y,第二个数字(01)的最后2 位移出范围,不计入计算.

第二个添加添加x + yz添加两个相同比例的数字.

第二组补充

在这里,x + z先发生.它们具有相同的比例,但它们产生的数字在规模上更高:

404 => 0|100 0000 0100| => 1028 - 1023 = 5
Run Code Online (Sandbox Code Playgroud)

第二个添加添加x + zy,现在删除3y以添加数字(101).在这里,必须有向上舍入,因为结果是下一个浮点数向上:4047866666666666对于第一组加法与4047866666666667第二组加法.该错误足以显示在总数的打印输出中.

总之,在对IEEE数字执行数学运算时要小心.一些表示是不精确的,当尺度不同时,它们变得更加不精确.如果可以,添加和减去相似比例的数字.


Eri*_*ert 44

乔恩的回答当然是正确的.在您的情况下,错误不会大于您在执行任何简单浮点操作时累积的错误.你有一个场景,在一种情况下,你得到零错误,在另一种情况下,你得到一个小错误; 这实际上并不是一个有趣的场景.一个很好的问题是:是否存在将计算顺序从微小错误变为(相对)巨大错误的情况?答案是毫无疑问的.

考虑例如:

x1 = (a - b) + (c - d) + (e - f) + (g - h);
Run Code Online (Sandbox Code Playgroud)

VS

x2 = (a + c + e + g) - (b + d + f + h);
Run Code Online (Sandbox Code Playgroud)

VS

x3 = a - b + c - d + e - f + g - h;
Run Code Online (Sandbox Code Playgroud)

显然,在精确算术中,它们将是相同的.尝试找到a,b,c,d,e,f,g,h的值使得x1和x2和x3的值相差很大是很有趣的.看看你能否这样做!

  • @frozenkoi:是的,错误很容易变得无限.例如,考虑C#:`double d = double.MaxValue; Console.WriteLine(d + d - d - d); Console.WriteLine(d - d + d - d);` - 输出为无穷大,然后为0. (8认同)
  • @Cruncher:计算确切的数学结果以及x1和x2值.调用真实和计算结果e1和e2之间的确切数学差异.现在有几种方法可以考虑错误大小.第一个是:你能找到一个场景,其中包括| e1/e2 | 或者 e2/e1 | 很大?比如,你可以犯下另一个错误十倍的错误吗?更有趣的是,如果你可以使一个误差的大小与正确答案大小相当. (3认同)

Com*_*ass 10

这实际上涵盖的不仅仅是Java和Javascript,并且可能会影响使用浮点数或双精度的任何编程语言.

在内存中,浮点使用IEEE 754的特殊格式(转换器提供了比我更好的解释).

无论如何,这是浮动转换器.

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的结果.


jbx*_*jbx 9

浮点数使用IEEE 754格式表示,该格式为尾数(有效数字)提供特定的位大小.不幸的是,这会为您提供特定数量的"分数构建块",并且某些小数值无法精确表示.

在您的情况下发生的情况是,在第二种情况下,由于评估添加的顺序,添加可能会遇到一些精确问题.我没有计算出这些值,但可能是23.53 + 17.64不能精确表示,而23.53 + 5.88可以.

不幸的是,这是一个已知的问题,你只需要处理.


hot*_*ure 6

我认为这与评估的顺序有关.虽然数学世界中的总和自然是相同的,但在二进制世界中,而不是A + B + C = D,它就是

A + B = E
E + C = D(1)
Run Code Online (Sandbox Code Playgroud)

所以浮动点数可以下降的第二步.

当您更改订单时

A + C = F
F + B = D(2)
Run Code Online (Sandbox Code Playgroud)

  • 我认为这个答案避免了真正的原因."浮点数可以下降的第二步".显然,这是事实,但我们要解释的是*为什么*. (4认同)