不同编程语言中的浮点数运算

36

我知道浮点数计算可能非常棘手,但我想知道是否有人可以解释以下怪现象。在我测试的大多数编程语言中,将0.4加上0.2会产生轻微误差,而0.4 + 0.1 + 0.1则不会。

两个计算结果不同的原因是什么?在相应的编程语言中,可以采取哪些措施来获得正确的结果。

在Python2/3中

.4 + .2
0.6000000000000001
.4 + .1 + .1
0.6

在 Julia 0.3 中也是如此。

julia> .4 + .2
0.6000000000000001

julia> .4 + .1 + .1
0.6

以及 Scala:

scala> 0.4 + 0.2
res0: Double = 0.6000000000000001

scala> 0.4 + 0.1 + 0.1
res1: Double = 0.6

以及 Haskell:

Prelude> 0.4 + 0.2
0.6000000000000001    
Prelude> 0.4 + 0.1 + 0.1
0.6

但是 R v3 做得很好:

> .4 + .2
[1] 0.6
> .4 + .1 + .1
[1] 0.6

14
有些语言会在展示时四舍五入隐藏真相。 - DavidO
18
实际上,R 只是将其隐藏起来了:运行 format(.4 + .1 + .1, digits=17)format(.4 + .2, digits=17) - tonytonov
3
这些结果使用IEEE标记法尽可能正确。 - Guntram Blohm
3
浮点数计算最好的情况远非丑陋;它已被证明足够美丽,足以让人登上月球,在模拟人类心脏运作和探索宇宙最遥远的深处。任何丑陋都在(近视、散光)观察者眼中。 - High Performance Mark
2
@Mark 浮点数很棒,只是没有定义精确相等。那些给程序员提供这种功能的语言正在犯下小小的谎言。事实证明,许多现实世界的情况也没有精确的相等性。 - J. Abrahamson
显示剩余6条评论
3个回答

52
所有这些语言都使用系统提供的浮点数格式,在其中用二进制表示值,而不是十进制。 值如0.2和0.4在该格式中无法准确表示,因此存储最接近的可表示值,从而产生小误差。 例如,数字文字0.2导致浮点数,其精确值为0.200000000000000011102230246251565404236316680908203125。 同样,对浮点数进行的任何给定算术操作可能会导致不能准确表示的值,因此真实的数学结果将被替换为最接近的可表示值。 这些是您看到的错误的根本原因。
然而,这并不能解释语言之间的差异:在您的所有示例中,进行了完全相同的计算,并得出了完全相同的结果。 那么区别在于各种语言选择如何显示结果。

严格来说,你展示的所有答案都不正确。假设使用IEEE 754二进制64位算术和四舍五入模式,第一个求和的确切值为:

0.600000000000000088817841970012523233890533447265625

而第二个总和的确切值为:

0.59999999999999997779553950749686919152736663818359375

然而,这些输出都不太用户友好,显然所有你测试过的语言在打印时都要缩写输出。但是,它们并没有采用相同的格式化策略,这就是你看到差异的原因。
有许多可能的格式化策略,但三种特别常见的是:
  1. 计算并显示17位正确舍入的有效数字,可能会剥离尾随零。17位数字的输出保证不同的binary64浮点数将具有不同的表示形式,因此可以从其表示中明确地恢复浮点值;17是具有此属性的最小整数。例如,Python 2.6使用这种策略。

  2. 计算并显示最短的十进制字符串,根据通常的“四舍六入五成双”舍入模式回舍为给定的binary64值。这比第1个策略更复杂,但保留了不同浮点数具有不同表示的属性,并且倾向于产生更愉悦的输出。这似乎是除R之外你测试过的所有语言都在使用的策略。

  3. 计算并显示15(或更少)位正确舍入的有效数字。这将隐藏涉及十进制到二进制转换的误差,给出精确十进制算术的错觉。它的缺点是不同的浮点数可以具有相同的表示形式。这似乎是R正在做的事情。(感谢@hadley在评论中指出R设置可以控制用于显示的数字位数;默认设置是使用7个有效数字。)


@hadley:谢谢。我试图在文档中找到这些信息;你有相关的文档链接吗? - Mark Dickinson
在R中,使用?options命令,然后找到'digits'选项。在线文档地址:http://stat.ethz.ch/R-manual/R-patched/library/base/html/options.html - Gray
3
非常好的解释。以最少的十进制数字打印二进制浮点数值,以便在输入时产生相同的值,这是一个令人惊讶地难题。一种高效的算法,不需要任意精度算术,在2010年才由Florian Loitsch发布(http://florian.loitsch.com/publications/dtoa-pldi2010.pdf)。Julia使用了优秀的double-conversion库(https://code.google.com/p/double-conversion/),该库是由Florian为V8 JavaScript引擎开发的。 - StefanKarpinski
@StefanKarpinski 对于某些情况,它仍需要任意精度(来自参考文献的说法:“...大约有99.5%被正确处理,因此保证是最优的(关于短和舍入)。剩下的0.5%被拒绝并需要由另一个打印算法(如Dragon4)打印。”)。 - Rick Regan
是的,这是真的。或者你可以在完美最优的打印上稍微放弃一些,不用它。 - StefanKarpinski
显示剩余2条评论

7
你需要知道的是,0.60.40.20.1在IEEE浮点数中无法被精确表示。这是因为比率1/5在二进制中是一个无限重复的小数,就像在十进制中1/31/7等比率一样。由于你的初始常量都不是精确的,所以你的结果也不是精确的,这并不令人意外。(注意:如果你想更好地理解这种不精确性,请尝试从计算结果中减去你预期的值...)
同样,还有其他一些潜在的陷阱。例如,浮点运算只是近似的结合律:以不同的顺序将相同的一组数字相加通常会给出稍微不同的结果(有时甚至会给出非常不同的结果)。因此,在需要精度的情况下,你应该小心累加浮点值的方式。

在这种情况下,通常的建议是阅读"计算机科学家应该了解的浮点运算知识", 作者是David Goldberg。要点是:浮点数不是精确的,对其行为的天真假设可能得不到支持。


4
原因是由于根据IEEE浮点算术标准,它在最后被四舍五入了。 根据该标准:加法,乘法和除法应完全正确,一直到最后一位。 这是因为计算机具有有限的空间来表示这些值,并且不能无限地追踪精度。请参考:http://en.wikipedia.org/wiki/IEEE_754

2
“不能无限地跟踪零” - 这很容易理解。使用高效的编码方式,无限数量的0占用0存储空间,因为它不包含任何信息。问题在于如何存储无限数量的混合0和1的轨迹。 - user2357112
2
说实话,“不能无限地拖着零”这个短语没有任何意义。在IEEE 754格式中,所有数字在十进制和二进制中都有无限的尾随零,因此显然可以表示具有此属性的数字。 - Pascal Cuoq
@PascalCuoq,这里是修正后的措辞。 - Nowayz

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接