为什么(0.0006*100000)%10等于10?

4

当我在Python中执行(0.0006*100000)%10和(0.0003*100000)%10时,分别返回9.999999999999993,但实际上应该是0。 类似地,在C++中执行fmod(0.0003*100000,10)会将值设置为10。请问有人能帮我找出问题所在吗。


3
这就是浮点数的工作原理。由于数字系统中的所有浮点值都是近似值,因此您永远不会获得完美的准确性。 - Marc B
3
@MarcB,你可以获得完美的准确性,只是不包括所有数字。 - tangrs
4
@Depado,你总是能得到一个精确的结果。它可能不是你想要的结果,并且通常与如果你在实数上进行相同的算术运算得到的结果不同,但它是精确的和完全确定的。机器浮点数不是实数,并遵循不同的规则。 - James Kanze
2
还有,我是唯一一个在浮点数取模算术中感到不安的人吗? - tangrs
4
@tangrs,无论是0.0002还是0.0005都无法精确表示。它们的值略高于实际值:分别为0.00020000000000000000958434720477185919662588275969028472900390625和0.0005000000000000000104083408558608425664715468883514404296875。 - Patricia Shanahan
显示剩余14条评论
3个回答

10

最接近0.0003的IEEE 754 64位二进制数是0.0002999999999999999737189393389513725196593441069126129150390625。将其乘以100000得到的最接近可表示数字是29.999999999999996447286321199499070644378662109375。

有许多操作,如floor和mod,可以使非常低的差异非常明显。在使用它们与浮点数连接时需要小心 - 记住,在许多情况下,您具有无限精度值的非常接近的近似值,而不是无限精度值本身。实际值可能略高或略低,就像这种情况一样。


5

很明显,0.00060.0003在机器双精度浮点数中无法表示(至少在现代计算机上)。因此,您实际上并没有乘以这些值,而是乘以非常接近的某个值。具体取决于编译器如何进行四舍五入,可能会略微增加或减少。


0

我可以建议在C中使用余数函数吗?

它将计算商四舍五入到最近的整数后的余数,进行精确计算(无舍入误差):

remainder = dividend - round(dividend/divisor)*divisor

这样,你的结果将在[-divisor/2,+divisor/2]区间内。
这仍然强调了你得不到一个完全等于6/10,000的浮点数,但当你期望一个零余数时,可能会以一种不那么令人惊讶的方式呈现:

remainder(0.0006*100000,10.0) -> -7.105427357601002e-15
remainder(0.0003*100000,10.0) -> -3.552713678800501e-15

我不知道Python是否支持这样的余数函数,但在gnulib-python模块中似乎有一个匹配项(待验证...)
https://github.com/ghostmansd/gnulib-python/blob/master/modules/remainder

编辑 为什么在[1,9]区间的每个N/10,000都能工作,除了3和6?

这并不完全是运气,这是IEEE 754在默认舍入模式(四舍五入,遇到奇数时向偶数靠拢)下的一些好特性。

浮点运算的结果会被舍入为最接近的浮点值。
因此,你得到的不是N/D,而是(N/D+err),其中绝对误差err由以下代码片段给出(我更熟悉Smalltalk,但我相信你可以在Python中找到等效的代码):

| d |
d := 10000.
^(1 to: 9) collect: [:n | ((n/d) asFloat asFraction - (n/d)) asFloat]

它会给你类似这样的东西:
#(4.79217360238593e-21 9.58434720477186e-21 -2.6281060661048628e-20 1.916869440954372e-20 1.0408340855860843e-20 -5.2562121322097256e-20 -7.11236625150491e-21 3.833738881908744e-20 -2.4633073358870662e-20)

改变浮点数尾数的最后一位会导致一个名为最小精度单位(ulp)的小差异,用ulp来表示误差可能是一个不错的选择:
| d |
d := 10000.
^(1 to: 9) collect: [:n | ((n/d) asFloat asFraction - (n/d)) / (n/d) asFloat ulp]

ulp离精确分数的数量如下:

#(0.3536 0.3536 -0.4848 0.3536 0.096 -0.4848 -0.0656 0.3536 -0.2272)

当N=1,2,4,8时,错误是相同的,因为它们本质上是相同的浮点数-相同的尾数,只是指数不同。
由于同样的原因,当N=3和6时也是如此,但非常接近单个操作的最大误差,即0.5 ulp(不幸的是,数字可能正好处于两个浮点数之间)。
对于N=9,相对误差比N=1小,对于5和7,误差非常小。

现在,当我们将这些近似值乘以10000,这个数可以被表示为一个浮点数,(N/D+err)D is N+Derr,然后四舍五入到最近的浮点数。如果D*err小于下一个浮点数的一半距离,则会将其舍入为N,并且舍入误差会消失。

| d |
d := 10000.
^(1 to: 9) collect: [:n | ((n/d) asFloat asFraction - (n/d)) * d / n asFloat ulp]

好的,对于N=3和6,我们很不幸,已经很高的舍入误差幅度已经大于0.5 ulp:

#(0.2158203125 0.2158203125 -0.591796875 0.2158203125 0.1171875 -0.591796875 -0.080078125 0.2158203125 -0.138671875)

注意,对于精确的二次幂,距离并不对称,1.0之后的下一个浮点数是1.0+2^-52,但在1.0之前是1.0-2^-53。

尽管如此,我们在第二次舍入操作后看到的是,误差在四种情况下被消除,而只在一种情况下累积(仅计算具有不同有效数字的情况)。

我们可以将该结果推广。只要我们不对具有非常不同指数的数字进行求和,而只使用乘法/除法运算,虽然在P次操作后误差界可能很高,但累积误差的统计分布与该界相比具有明显狭窄的峰值,并且结果与我们通常读到的浮点数不精确的情况相比,令人惊讶地好。例如,请参见我的回答带有大量术语的双倍产品中正确十进制位数的数量

我只是想提一下,是的,浮点数是不精确的,但有时它们做得很好,以至于它们会营造出精确性的错觉。像这篇文章中提到的那样找到一些异常值是令人惊讶的。越早惊讶,就越少惊讶。啊,如果浮点数实现得不那么小心,这个类别中就会有更少的问题...


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