当我在Python中执行(0.0006*100000)%10和(0.0003*100000)%10时,分别返回9.999999999999993,但实际上应该是0。 类似地,在C++中执行fmod(0.0003*100000,10)会将值设置为10。请问有人能帮我找出问题所在吗。
当我在Python中执行(0.0006*100000)%10和(0.0003*100000)%10时,分别返回9.999999999999993,但实际上应该是0。 类似地,在C++中执行fmod(0.0003*100000,10)会将值设置为10。请问有人能帮我找出问题所在吗。
最接近0.0003的IEEE 754 64位二进制数是0.0002999999999999999737189393389513725196593441069126129150390625。将其乘以100000得到的最接近可表示数字是29.999999999999996447286321199499070644378662109375。
有许多操作,如floor和mod,可以使非常低的差异非常明显。在使用它们与浮点数连接时需要小心 - 记住,在许多情况下,您具有无限精度值的非常接近的近似值,而不是无限精度值本身。实际值可能略高或略低,就像这种情况一样。
很明显,0.0006
和0.0003
在机器双精度浮点数中无法表示(至少在现代计算机上)。因此,您实际上并没有乘以这些值,而是乘以非常接近的某个值。具体取决于编译器如何进行四舍五入,可能会略微增加或减少。
我可以建议在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)
| 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次操作后误差界可能很高,但累积误差的统计分布与该界相比具有明显狭窄的峰值,并且结果与我们通常读到的浮点数不精确的情况相比,令人惊讶地好。例如,请参见我的回答带有大量术语的双倍产品中正确十进制位数的数量。
我只是想提一下,是的,浮点数是不精确的,但有时它们做得很好,以至于它们会营造出精确性的错觉。像这篇文章中提到的那样找到一些异常值是令人惊讶的。越早惊讶,就越少惊讶。啊,如果浮点数实现得不那么小心,这个类别中就会有更少的问题...