雪花飘落

浮点数运算的一些问题

引入

先来看个代码:

print(1-0.7 == 0.3)

很多人会觉得这一看不就是True吗,但实际上结果为False。因为1-0.7的结果为0.30000000000000004

浮点数转二进制的方法

可以用这个网站验证答案:https://c.runoob.com/front-end/58/

因为所有的数据本质都是通过二进制数存储的,所以要分析这个问题的本质,我们得先去看浮点数的二进制表示。因为整数和小数的转换方法不同,所以先将十进制数的整数部分和小数部分分别转换后,再加以合并。

(1)整数部分:整数部分比较简单,因为一个有限的十进制整数都能转换成有限的二进制数。采用除2取余,逆序连接的方法,代码如下:

1
2
3
4
5
6
7
n = 173
ls = []
while n != 0:
    ls.append(str(n%2))
    n//=2
ls.reverse()
print(''.join(ls))

(2)小数部分:小数部分会麻烦一点了,具体的方法为:

复制代码
'''
如:0.625=(0.101)bin
0.625*2=1.25======取出整数部分1
0.25*2=0.5========取出整数部分0
0.5*2=1==========取出整数部分1
直到积中的小数部分为零为止,从上倒下连接
'''
import math

n = 0.625
s = ''
while int(n)!= n:
    n*=2
    s+=str(int(n))
    n = math.modf(n)[0]
print(s)
复制代码

在这里可以发现,十进制的小数转二进制的时候,很容易就会无限循环下去,例如0.2转换成二进制就i是0011001100110011001100……,由于计算机存储的位是有限的,所以必然会造成精度的损失。

IEEE 754 

这里就不得不提到IEEE 754 了,这东西其实就是一个浮点数的运算标准。因为对于有符号整数来说,存储的方式其实很简单,一位表示符号,剩下的位都可以用来表示数值,但浮点数的情况比较复杂。对于float32,也就是32位的浮点数,是这样表示的:V = (-1)^S*M*2^E

 (1)(-1)^s 表示符号位,当 s=0,V 为正数;当 s=1,V 为负数

 (2)M称为尾数,1≤ M <2,也就是说,M 可以写成 1.xxxxxx 的形式,其中 xxxxxx 表示小数部分。

 (3)E表示阶码。例如浮点数5.0,用二进制表示是101.0,用科学计数法表示为1.01*2^2(注意,因为这里是二进制数,所以是乘2的2次方而不是10的二次方)。此时阶码E就是2,尾数M为1.01

对于 32 位的浮点数,最高的 1 位是符号位 s,接着的 8 位是指数 E,剩下的 23 位为有效数字 M。

尾数

因为M 总是写成 1.xxxxxx *2^E的形式,所以在计算机内部保存 M 时,默认这个数的第一位总是 1,因此可以被舍去,只保存后面的 xxxxxx 部分。比如保存 1.01 的时候,只保存小数部分的 01,等到读取的时候,再把第一位的1加上去。这样做的目的,是节省 1 位有效数字。以32位浮点数为例,留给 M 只有 23 位,将第一位的1舍去以后,等于可以保存 24 位有效数字。

阶码

上面说到了,阶码是一个八位的二进制数,并且它是一个无符号数,所以取值范围是[0,255]。但是科学计数法中的指数并不一定都是正数,例如十进制0.5转换为二进制是0.1,因为1<=M<2,所以要写成1*2^-1,所以阶码部分,采用移码来表示。简单地讲,就是阶码的真实值为计算机存储值减去中间数127,比如,2^10 的 E 是 10,所以保存成 32 位浮点数时,必须保存成 10+127=137,即 10001001,这样阶码的范围就变成了[-127,128]。阶码这里又有一些特殊情况:

(1)阶码E全为0,有效数字 M 不再加上第一位的 1,而是还原为 0.xxxxxx 的小数。这样做是为了表示机器零(计算机中小到机器数的精度达不到的数均视为“机器零”),以及接近于 0 的很小的数字。

(2)阶码E全为1,阶码全为1的情况有两种,用来表示特殊值,第一种是尾数M也全为1,这时表示正负无穷大(正还是负取决于符号位)。若M不全为1,则表示NaN(Not a Number,非数,这个概念也是IEEE 754定义的),例如0除以0的结果。

1-0.7==0.3为什么为False

上面我们解释了为什么1-0.7的结果是0.30000000000000004了,由于计算机存储浮点数的位数有限,所以浮点数转成二进制表示的时候,必然会造成精度的损失,例如0.3转换为二进制表示为:

0.0100110011001100110011001100110011001100110011001101(舍入后的的结果),而把这一串再转换为十进制就是0.30000000000000004

 

而我们自定义的0.3,虽然本质上也是二进制存储的,它实际上也是0.30000000000000004,但是为了保证浮点数运算运行的结果大部分时候与用户预期一致,所以程序在以十进制表示这个数据的时候,会按照用户自定义的精度对数据进行舍入。例如我们定义a=0.3,那么在对变量a取值的时候,程序就会把0.30000000000000004舍入为0.3,保持输入与输出一致。但是在进行1-0.7计算的时候,因为用户并没有指定期望的结果精度,所以对于运算结果,程序不会做限制,直接取得0.30000000000000004。

 遗留的问题

使用加减法很容易出问题,但乘除法不会。

print(1-0.9)#0.09999999999999998
print(0.2/2)#0.1

至于是为什么,暂时还搞不清楚,以后来填坑

posted @   haruyuki  阅读(322)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· Docker 太简单,K8s 太复杂?w7panel 让容器管理更轻松!
点击右上角即可分享
微信分享提示