4th
浮点数运算的精度损失
Last updated
Was this helpful?
浮点数运算的精度损失
Last updated
Was this helpful?
接上一条
上次留了一个问题,就是为什么在编程语言中用 float 类型的 0.3 + 0.6,结果会是 0.899999…,而不是 0.9。
要想弄明白这个问题,必须先搞清楚两件事:
十进制小数如何转换成二进制浮点数
浮点数如何做加法运算
十进制小数 → 二进制浮点数
首先说一下, 十进制小数如何转换成二进制浮点数。十进制小数要想转换成二进制,要把小数部分乘以 2,乘积记做 r,如果 r > 1,则记下 1,令 ,继续循环操作;如果 r < 1,则记下 0,令 ,继续循环操作;如果 r = 1,则记下 1,计算结束。计算过程的伪代码如下:
当我们尝试把十进制的 0.3 转换成二进制时,你会发现,我们得到的是 0.0100110011…,这里的「0011」会无限循环下去,如果用上面这段伪代码去计算 0.3 的二进制,它会陷入死循环,永远无法结束!按照上次讲的 IEEE-754 标准,0.3 应该表示成 ,如果在计算机中存储为 32 位的浮点数,即 float,就是这样:
虽然小数点部分的「0011」是无限循环的,但是计算机的位数是有限的,以 float 为例,小数点后的有效数位长度只有 23,超过 23 位的部分都被「截」掉了,所以计算机是无法精确表示 0.3 的。虽然 64 位浮点数 double 的有效数位更长,但是对于小数点后无限循环的二进制小数来说,double 同样无法精确表示,顶多就是精度比 float 更高而已。
如果你仔细观察,会发现在上图中的有效数位 f 中,最右边的三位是 010。如果按照 00110011… 这样循环下去,取前 23 位,最后得到的数应该是 00110011001100110011001,那么最右边三位应该是 001 才对啊,怎么是 010 呢?这其实涉及到了浮点数的舍入问题。
上面这张图展示了 0.3 的有效数位,上面那行(即白色背景)数字是实际存储在计算机中的,也就是舍入后的,下面那行(绿色背景 + 橙色背景)是舍入前的,其中橙色背景的是需要舍入的部分。默认的舍入规则是舍入到最接近,一样接近的情况下偶数优先。比如上面那张图中,直接舍掉的话结果是 00110011001100110011001,与直接舍掉相比,舍入前的数字显然更接近 00110011001100110011010(不要忘了我们讨论的只是小数部分,前面还有一个默认的「1.」),所以我们最终得到的有效数位 f 就是 00110011001100110011010。
还有一种情况,就是舍入前的有效位数不是无限循环的,比如这样:
这种情况下,只有最右边的 1 需要舍入,而 0.001100110011001100110011 与 0.00110011001100110011001 和 0.00110011001100110011010 的距离是一样的,都相差 0.000000000000000000000001,这时就按照偶数优先的原则,让 1 进位,得到 0.00110011001100110011010。
IEEE 标准总共列出了 4 种不同的方法:
舍入到最接近:舍入到最接近,在一样接近的情况下偶数优先(Ties To Even,这是默认的舍入方式):会将结果舍入为最接近且可以表示的值,但是当存在两个数一样接近的时候,则取其中的偶数(在二进制中是以 0 结尾的)。
朝 +∞ 方向舍入:会将结果朝正无限大的方向舍入。
朝 -∞ 方向舍入:会将结果朝负无限大的方向舍入。
朝 0 方向舍入:会将结果朝 0 的方向舍入。
同理,0.6 的 float 表示形式为:
浮点数如何做加法运算
浮点数做加法运算遵循一个原则,那就是先对齐,再计算。
「对齐」指的是对齐指数位 e。两个浮点数相加时,如果 e 不一样,就要先把 e 对齐再做加法运算。如何对齐呢?对齐的原则就是把 e 都统一成其中较大的一个。
比如 0.5 与 0.125 相加,这两个数表示成浮点数分别是 和 ,由于 0.5 的指数大于 0.125 的指数,所以要把 0.125 的指数统一成和 0.5 一样的 -1,指数增大,那么有效数位要右移,因为 f 前面默认有个 1,所以右移之后,0.125 的有效数位 变成了 01,然后将 0.5 的有效数位与 0.125 的有效数位相加,结果还是 01,最终表示成浮点数就是 。
如果有效数位相加的过程中得到的结果大于 1 了,那么要让有效数位进行右移,同时指数增加相应的值,右移了几位,指数就加几。右移时必然会有丢失的有效数,如果这些有效数都是 0 还好,不会有影响,如果丢失的有效数中包含 1,就要按照前面讲的舍入规则进行舍入,从而造成了精度损失。
如果两个相加的单精度浮点数的指数位相差超过了了 23,也就是差不多 1600 万倍,那么相加的结果就等于较大的那个数。你可以在 Java 中用 float 类型的 2000 万加 1,循环 1000 万次,看看结果是多少。
现在我们来看一下为什么 0.3 + 0.6 = 0.8999999999999999。
开头那张图我是用 Python3 算的,Python3 默认是使用 64 位的浮点数,所以 0.3 和 0.6 的浮点数表示形式如下:
0.3 :
0.6:
注意,这个时候其实计算机中存的已经不是精确的 0.3 和 0.6 了,然后按照我们刚才讲的浮点数加法运算的规则,先对齐,再计算,得到的结果是:
把这个结果换算成十进制,就是 0.8999999999999999。
如何避免浮点数运算带来的精度损失也是一个比较有意思的话题,大家感兴趣的话可以搜一下 Kahan Summation 算法。
参考资料:
《计算机组成与设计:硬件 / 软件接口》3.5.1 节