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


294

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

23.53 + 5.88 + 17.64 = 47.05

23.53 + 17.64 + 5.88 = 47.050000000000004

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

我知道,由于以二进制表示浮点数的方式,某些有理数(例如1/3-0.333333 ...)无法精确表示。

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


28
实数之和是关联的和可交换的。浮点数不是实数。实际上,您只是证明了它们的运算不是可交换的。很容易证明它们也不具有关联性(例如(2.0^53 + 1) - 1 == 2.0^53 - 1 != 2^53 == 2^53 + (1 - 1))。因此,是的:在选择和顺序和其他运算时要小心。某些语言提供了执行“高精度”求和的内置math.fsum函数(例如python ),因此您可以考虑使用这些函数而不是朴素的求和算法。
Bakuriu

1
@RBerteig可以通过检查语言对算术表达式的运算顺序来确定,除非它们在内存中的浮点数表示形式不同,否则如果它们的运算符优先级规则相同,则结果将相同。还要注意的另一点是:我想知道开发银行应用程序的开发人员花了多长时间才能解决这个问题?那些额外的0000000000004美分真的加起来了!
克里斯·西里菲斯

3
@ChrisCirefice:如果您有0.00000004 美分,那就错了。你应该永远不会使用财务计算二进制浮点类型。
Daniel Pryden

2
@DanielPryden啊,这是个笑话...只是在抛出一个想法,就是那些真正需要解决这类问题的人拥有了您所知道的最重要的工作之一,拥有着人们的金钱状况以及所有这些。 。我当时很讽刺……
克里斯·西里菲斯

6
非常干燥(很古老,但仍然很重要):每位计算机科学家都应该了解浮点运算法则
Brian

Answers:


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)

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

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值,由于上面显示的问题,这只是一个问题。


10
May extend this later - out of time right now!急切地等待着它@Jon
Prateek

3
当我说以后我会再回答时,社区对我的友善程度稍差一些。在这里输入某种轻松的表情符号来表明我在开玩笑而不是一个混蛋。
Grady Player

2
@ZongZhengLi:了解这一点固然很重要,但这并不是本例的根本原因。你可以写与值类似的例子正好在二进制表示,看到同样的效果。这里的问题是同时维护大型信息和小型信息。
乔恩·斯基特

1
@Buksy:四舍五入到10000 -因为我们正在处理的数据类型只能存储4个显著数字。(因此x.xxx * 10 ^ n)
乔恩·斯基特

3
@meteors:不,它不会引起溢出-您使用的是错误的数字。将10001舍入为10000,而不是1001舍入为1000。更清楚地说,将54321舍入为54320-因为它只有四个有效数字。“四个有效数字”与“最大值9999”之间存在很大差异。正如我以前说过,你基本上代表x.xxx * 10 ^ n,其中10000,x.xxx是1.000,和n将是4。这就像doublefloat,其中非常大的数字,连续的表示数字相隔1个以上。
乔恩·斯基特

52

这是二进制中发生的事情。众所周知,即使某些浮点值可以精确地用十进制表示,也不能精确地用二进制表示。这三个数字只是该事实的示例。

通过此程序,我输出每个数字的十六进制表示形式以及每个加法的结果。

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个数字是xyz,和s的十六进制表示。在IEEE浮点表示中,位2-12表示二进制指数,即数字的小数位数。(第一位是符号位,其余为尾数位。)所表示的指数实际上是二进制数减去1023。

提取前四个数字的指数:

    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 + yz相加两个相同比例的数字。

第二套加法

在这里,x + z首先发生。它们具有相同的规模,但是它们产生的数字规模更大:

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

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

总之,对IEEE数字执行数学运算时要小心。有些表示形式不精确,当比例不同时,它们甚至变得更加不精确。如果可以的话,可以加减类似比例的数字。


比例尺是重要的部分。您可以写(用十进制表示)以二进制表示的精确值作为输入,但仍然存在相同的问题。
乔恩·斯基特

@rgettman作为程序员,我希望您=)的十六进制打印机助手的答案更好+1…… 真是太好了!
ADTC

44

乔恩的答案当然是正确的。在您的情况下,该错误不大于在执行任何简单的浮点运算时将累积的错误。您有一种情况,在一种情况下,您将得到零错误,而在另一种情况下,您将得到很小的错误。这实际上不是那么有趣的情况。一个好问题是:是否存在将计算顺序从微小错误变为(相对)巨大错误的情况?答案是肯定的。

考虑例如:

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的值相差很大。看看是否可以这样做!


您如何定义大量?我们说的是千分之一吗?100分?1的???
Cruncher

3
@Cruncher:计算确切的数学结果以及x1和x2值。调用真实结果e1和计算结果e2之间的确切数学差。现在有几种方法可以考虑错误大小。第一个是:您是否可以找到以下任一情况?e1 / e2 | 或| e2 / e1 | 大吗?就像,您能否使一个错误的误差是另一个错误的十倍?但是,更有趣的是,如果您可以使一个错误的大小占正确答案大小的一大部分。
埃里克·利珀特

1
我意识到他在谈论运行时,但是我想知道:如果该表达式是一个编译时(例如constexpr)表达式,那么编译器是否足够聪明以最小化错误?

@kevinhsu通常不,编译器不是那么聪明。当然,如果选择了编译器,则可以选择使用精确的算术运算,但是通常不会。
埃里克·利珀特

8
@frozenkoi:是的,错误很容易无限大。例如,考虑了C#:double d = double.MaxValue; Console.WriteLine(d + d - d - d); Console.WriteLine(d - d + d - d);-输出是无限然后0
乔恩斯基特

10

实际上,这不仅涉及Java和Javascript,而且可能会影响使用float或double的任何编程语言。

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

无论如何,这是float转换器。

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


9

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

在您的情况下发生的是,在第二种情况下,由于对添加项进行评估的顺序,添加项可能会遇到一些精度问题。我还没有计算出值,但是举例来说,可能无法精确表示23.53 + 17.64,而可以精确表示23.53 + 5.88。

不幸的是,这是一个已知问题,您只需解决。


6

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

A + B = E
E + C = D(1)

因此,存在第二步,浮点数可以下浮。

当您更改订单时,

A + C = F
F + B = D(2)

4
我认为这个答案避免了真正的原因。“有第二个步骤,浮点数可以下浮”。显然,这是事实,但是我们要解释的是为什么
Zong
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.