为什么这个表达式会导致浮点数错误?

3

因此,浮点运算是不精确的,但这并不能完全解释正在发生的事情:

[46] pry(main)> a=0.05
=> 0.05
[47] pry(main)> a=a*26.0/65
=> 0.02

所以现在我们得到了预期的结果,我们得到了正确的答案,世界依然美好地运转着。但是后来我们重写了这个函数,在此过程中我们将a=a*26.0/65替换为a*=26.0/65,这不是很好吗,我们少打了一个字符!让我们看看这对我们有什么影响?

[48] pry(main)> a=0.05
=> 0.05
[49] pry(main)> a*=26.0/65
=> 0.020000000000000004
[50] pry(main)> 26.0/65
=> 0.4

它表明a*=ba=a*b不同。这似乎不是正常的浮点舍入误差,因为这些数字中没有一个应该被舍入为浮点数(尾数应该足够长以适应26.0、26.0/65、65.0中的每一个)。我确定在幕后有一些微妙的问题,想知道发生了什么?

1
使用Ruby 2.0.0p247和1.9.3p392 [x86_64-linux]进行复制。我喜欢“世界依然美丽地转动”的那部分 :) - tessi
2个回答

5
浮点格式的有效数字位数不足以表示26/65,这是不正确的。(“有效数字”是首选术语。有效数字是线性的。尾数是对数的。)二进制浮点数的有效数字是一个二进制整数,该整数根据指数进行缩放。要在二进制浮点数中表示26/65(即0.4),我们必须将其表示为乘以2的幂次方的整数。例如,0.4的近似值为1•2-1=0.5。更好的近似值是3•2-3=0.375。更好的是26•2-4=0.40625。但是,无论您使用什么整数作为有效数字或使用什么指数,该格式永远不能完全表示0.4。假设您有0.4=f•2e,其中fe是整数。那么2/5=f•2e,所以2/(5f)=2e,然后1/(5f)=2e-1,并且5f=21-e。为了使这成立,5必须是2的幂次方。但它不是,所以您不能有0.4=f•2e。在IEEE-754 64位二进制浮点数中,有效数字有53位。使用这个,最接近0.4的可表示值为0.40000000000000002220446049250313080847263336181640625,等于3602879701896397•2-53。现在让我们看看您的计算。在a=0.05中,0.05被转换为浮点数,产生0.05000000000000000277555756156289135105907917022705078125。
在表达式 a*26.0/65 中,首先计算 a*26.0。精确的数学结果四舍五入为最接近的可表示值,得到 1.3000000000000000444089209850062616169452667236328125。然后将此值除以 65。答案再次四舍五入,得到 0.0200000000000000004163336342344337026588618755340576171875。当 Ruby 打印这个值时,它显然认为它足够接近于 .02,只显示“.02”,而不是完整的值。从某种意义上说,这是合理的,因为如果你将打印出来的值 .02 转换回浮点数,你会再次得到实际值 0.0200000000000000004163336342344337026588618755340576171875。因此,“.02”在某种意义上是 0.0200000000000000004163336342344337026588618755340576171875 的一个很好的代表。
在你的替代表达式中,你有 a*=26.0/65。在这个表达式中,首先计算 26.0/65。这产生 0.40000000000000002220446049250313080847263336181640625。由于你按不同的顺序执行了操作,所以这与第一个表达式不同。可能发生的情况是,第一个表达式中的值被舍入,而因为此处相对于浮点数可表示的值的位置而定,所以舍入了不同的值。
然后将该值乘以 a。这产生 0.02000000000000000388578058618804789148271083831787109375。请注意,这个值比第一个表达式的结果更接近 .02。你的 Ruby 实现知道这一点,所以它确定仅打印“.02”无法准确地表示它。相反,它显示更多的数字,显示 0.020000000000000004。

1
+1 崇敬!讲解得非常好。我有点明白了,但你把它讲得清清楚楚的。 - Paulo Bu
我可以问一下你从哪里得到所有这些结果的精确值吗?你使用了计算或执行计算的工具吗?如果你用了Ruby,你是如何让它显示所有位数的呢?谢谢。 - Mike H-R
@MikeH-R:苹果的标准C库以及其余的C实现能够正确生成结果。因此,要获取浮点数的精确值,你只需要请求足够多的数字,比如使用printf("%.999g\n", x);。(并非所有的C实现都能做到这一点;有些可能只能正确计算出大约17位数字左右的结果。) - Eric Postpischil

3

我想我明白了这里发生了什么。看一下这段代码和操作顺序:

irb(main):001:0> a=0.05
=> 0.05
irb(main):002:0> b=26.0
=> 26.0
irb(main):003:0> c=65
=> 65
irb(main):004:0> a*b/c
=> 0.02
irb(main):005:0> a*(b/c)
=> 0.020000000000000004

在这里,a*b/c是解释器应该如何计算您的表达式a=a*26.0/65。它评估右侧,然后将结果分配给赋值的左侧。
现在,*=操作符到底是干什么的?如果我们强制修改操作的顺序a*(b/c),那么上面的代码显示你得到的结果就是a*=b/c,所以在内部,我认为Ruby的*=先评估表达式的左侧,然后乘以右侧,最后将其分配给右侧。
在我看来,这就是正在发生的事情。Ruby的解释器正在修改评估的方式,当然,因为我们处理的是非精确浮点数,这可能会对结果产生巨大影响,正如Jon Skeet在他对这个问题的惊人回答中所解释的那样:为什么改变求和顺序会返回不同的结果? 希望这能有所帮助!

好的,我相信你关于这个问题的答案是正确的,因为 [63] pry(main)> 0.05*0.4 => 0.020000000000000004,但我仍然感到困惑,根据我的浮点数算术知识,这不应该发生,这两个表示应该有足够的位来表示数字?我记得求和顺序很重要,但不知道它如何适用于这里?(再次感谢您提供的出色答案) - Mike H-R
我明白你的意思。让我们等待一些比我更了解浮点算术的人来解决这个谜题。我只是觉得没问题,因为在浮点算术中我总是做好最坏的打算 :) - Paulo Bu

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