ChatGPT解决这个技术问题 Extra ChatGPT

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

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

23.53 + 5.88 + 17.64 = 47.05

23.53 + 17.64 + 5.88 = 47.050000000000004

JavaJavaScript 都返回相同的结果。

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

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

实数和是结合和交换的。浮点数不是实数。实际上,您只是证明了它们的操作不是可交换的。很容易证明它们也不具有关联性(例如 (2.0^53 + 1) - 1 == 2.0^53 - 1 != 2^53 == 2^53 + (1 - 1))。因此,是的:在选择和的顺序和其他操作时要小心。某些语言提供了一个内置函数来执行“高精度”求和(例如 python 的 math.fsum),因此您可以考虑使用这些函数而不是简单求和算法。
@RBerteig这可以通过检查算术表达式的语言运算顺序来确定,除非它们在内存中的浮点数表示不同,否则如果它们的运算符优先级规则相同,结果将是相同的。还有一点需要注意:我想知道开发银行应用程序的开发人员花了多长时间才弄清楚这一点?那些额外的 0000000000004 美分真的加起来了!
@ChrisCirefice:如果你有 0.00000004 美分,那么你做错了。您永远不应该使用二进制浮点类型进行财务计算。
@DanielPryden啊,唉,真是个笑话……只是抛开这样的想法,即真正需要解决此类问题的人拥有您所知道的最重要的工作之一,掌握着人们的货币状况以及所有这些.我太讽刺了……
非常干燥(而且陈旧,但仍然相关):What Every Computer Scientist Should Know About Floating-Point Arithmetic

C
Community

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

它将根据值的大小更改值四舍五入的点。作为我们所看到的那种事情的一个例子,让我们假设我们使用的是具有 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 noted in his answer 问题的一个示例。)

请务必注意,右侧第一行的值在所有情况下都是相同的 - 因此,尽管了解您的十进制数(23.53、5.88、17.64)不会完全表示为 double 很重要值,这只是一个问题,因为上面显示的问题。


May extend this later - out of time right now! 热切地等待它@Jon
当我说我稍后会回复答案时,社区对我稍微不那么友善<在此处输入某种轻松的表情符号以表明我是在开玩笑而不是混蛋> ...稍后会回到这个问题。
@ZongZhengLi:虽然理解这一点当然很重要,但这不是这种情况的根本原因。您可以使用以二进制精确表示的值编写一个类似的示例,并看到相同的效果。这里的问题是同时维护大规模信息和小规模信息。
@Buksy:四舍五入到 10000 - 因为我们正在处理只能存储 4 个有效数字的数据类型。 (所以 x.xxx * 10^n)
@meteors:不,它不会导致溢出-而且您使用了错误的数字。它是 10001 被四舍五入到 10000,而不是 1001 被四舍五入到 1000。为了更清楚,54321 将被四舍五入到 54320 - 因为它只有四个有效数字。 “四位有效数字”和“最大值为 9999”之间存在很大差异。正如我之前所说,您基本上表示 x.xxx * 10^n,其中 10000,x.xxx 为 1.000,n 为 4。这就像 doublefloat,其中非常大数,连续可表示的数相距大于 1。
r
rgettman

这是二进制中发生的事情。众所周知,一些浮点值不能用二进制精确表示,即使它们可以用十进制精确表示。这 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);
   }
}

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 个数字是 xyzs 的十六进制表示。在 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 + yz 相加,并将两个相同比例的数字相加。

第二组补充

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

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

第二个添加添加了 x + zy,现在从 y 中删除 3 位以添加数字 (101)。在这里,必须向上取整,因为结果是下一个浮点数向上:4047866666666666 表示第一组加法,而 4047866666666667 表示第二组加法。该错误足以在总数的打印输出中显示出来。

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


不同的尺度是重要的部分。您可以写(十进制)以二进制表示的确切值作为输入,但仍然有同样的问题。
@rgettman 作为一名程序员,我更喜欢你的回答=) +1 为你的十六进制打印机助手...这真的很整洁!
E
Eric Lippert

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

考虑例如:

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 的值相差很大,这很有趣。看看你能不能做到!


如何定义大量?我们谈论的是千分之几吗?百分之一百? 1的???
@Cruncher:计算精确的数学结果以及 x1 和 x2 值。将真实结果和计算结果之间的精确数学差异称为 e1 和 e2。现在有几种方法可以考虑错误大小。第一个是:你能找到一个场景吗? e1 / e2 |或 | e2 / e1 |大吗?比如,你能让一个错误成为另一个错误的十倍吗?更有趣的是,如果您可以使一个错误占正确答案大小的很大一部分。
我意识到他在谈论运行时,但我想知道:如果表达式是编译时(例如,constexpr)表达式,编译器是否足够聪明以最小化错误?
@kevinhsu 一般不,编译器不是那么聪明。当然,编译器可以选择在精确算术中执行操作,但通常不会。
@frozenkoi:是的,错误很容易变得无限。例如,考虑 C#:double d = double.MaxValue; Console.WriteLine(d + d - d - d); Console.WriteLine(d - d + d - d); - 输出是 Infinity,然后是 0。
C
Compass

这实际上涵盖的不仅仅是 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 的结果。


j
jbx

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

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

不幸的是,这是您必须处理的已知问题。


h
hotforfeature

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

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

所以有第二个步骤,浮点数可以下车。

当您更改订单时,

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

我认为这个答案回避了真正的原因。 “有第二个步骤,浮点数可以下车”。显然,这是真的,但我们要解释的是为什么。
R
Richard

为了在此处为其他答案添加不同的角度,this SO answer 表明存在进行浮点数学运算的方法,其中所有求和顺序在位级别返回完全相同的值。