为什么fmod(1.0,0.1)等于0.1?
我最早在Python中遇到了这个现象,结果发现这是一个普遍的情况,比如说MS Excel也会这样。而Wolfram Alpha给出的答案有点奇怪,它说零的有理近似是1/5。(1.0 mod 0.1)
另一方面,如果我手动实现这个定义,就能得到“正确”的答案(0)。
def myFmod(a,n):
return a - floor(a/n) * n
这到底是怎么回事?我是不是漏掉了什么?
6 个回答
来自 man fmod
的内容:
fmod() 函数用来计算浮点数 x 除以 y 的余数。返回的结果是 x 减去 n 乘以 y,其中 n 是 x 除以 y 的商,向零取整得到的整数。
所以发生的事情是:
- 在
fmod(1.0, 0.1)
中,0.1 实际上比 0.1 稍微大一点,因为这个值不能被完全准确地表示为浮点数。 - 所以 n = x / y = 1.0 / 0.1000多一点 = 9.9999多一点
- 当向零取整时,n 实际上变成了 9
- 计算 x - n * y = 1.0 - 9 * 0.1 = 0.1
补充:至于为什么用 floor(x/y)
可以正常工作,我觉得这似乎是 FPU 的一个小怪癖。在 x86 架构上,fmod
使用的是 fprem
指令,而 x/y
则使用 fdiv
。有趣的是,1.0/0.1
似乎返回的正好是 10.0
:
>>> struct.pack('d', 1.0/0.1) == struct.pack('d', 10.0)
True
我猜 fdiv
使用的算法比 fprem
更精确。这里可以找到一些讨论: http://www.rapideuphoria.com/cgi-bin/esearch.exu?thread=1&fromMonth=A&fromYear=8&toMonth=C&toYear=8&keywords=%22Remainder%22
这个结果是因为计算机对浮点数的表示方式。在你的方法中,你是把浮点数“转换”成整数,所以没有遇到这个问题。为了避免这种问题(特别是在使用mod
的时候),最好的办法是先乘一个足够大的整数(在你的例子中只需要10),然后再进行操作。
fmod(1.0,0.1)
fmod(10.0,1.0) = 0
因为 0.1
其实并不是真正的0.1;这个值在双精度浮点数中无法准确表示,所以它被四舍五入到最接近的双精度数,这个数正是:
0.1000000000000000055511151231257827021181583404541015625
当你调用 fmod
时,你得到的是用上面那个值进行除法运算后的余数,结果正是:
0.0999999999999999500399638918679556809365749359130859375
当你打印出来时,这个值会四舍五入成 0.1
(或者可能是 0.09999999999999995
)。
换句话说,fmod
的工作是完全正确的,但你给它的输入并不是你想象中的那个。
编辑:你的实现能够给出正确的答案,实际上是因为它的精度更低,信不信由你。首先,注意 fmod
计算余数时没有任何四舍五入的误差;唯一的误差来源是使用 0.1
时引入的表示误差。现在,让我们一步一步分析你的实现,看看它产生的四舍五入误差是如何恰好抵消表示误差的。
我们逐步计算 a - floor(a/n) * n
,并记录每一步计算的确切值:
首先计算 1.0/n
,其中 n
是上面提到的最接近 0.1
的双精度近似值。这个除法的结果大约是:
9.999999999999999444888487687421760603063276150363492645647081359...
注意这个值并不是一个可以表示的双精度数——所以它被四舍五入了。为了看看这个四舍五入是怎么发生的,我们来用二进制表示这个数字,而不是十进制:
1001.1111111111111111111111111111111111111111111111111 10110000000...
空格表示四舍五入到双精度数的位置。由于小数点后的部分大于精确的中间值,这个值向上四舍五入到正好 10
。
floor(10.0)
当然是 10.0
。所以剩下的就是计算 1.0 - 10.0*0.1
。
在二进制中,10.0 * 0.1
的确切值是:
1.0000000000000000000000000000000000000000000000000000 0100
同样,这个值也不能表示为双精度数,因此在空格所示的位置被四舍五入。这次它向下四舍五入到正好 1.0
,所以最终的计算是 1.0 - 1.0
,结果当然是 0.0
。
你的实现包含了两个四舍五入误差,恰好抵消了 0.1
的表示误差。而 fmod
则是始终精确的(至少在有良好数值库的平台上),并且暴露了 0.1
的表示误差。