浮点数精度问题——由Mathf.Floor()引发的思考


前言

最近刚入职,在公司做功能的时候,遇到一个问题。就是在C#中:
Mathf.Floor(9999.9995f) = 9999
Mathf.Floor(9999.9996f) = 10000

这就让我很迷惑。

因为Mathf.Floor()方法的官方文档上面些的是取float的最近的小于等于它的整数,而10000比9999.9996大啊!

于是查阅了相关资料后,发现原来这一切都是浮点数精度搞的鬼!


什么是浮点数

要解释为什么,就先要从底层原理说起。

所谓浮点数,简而言之就是小数。

IEEE754规定浮点数分为单精度浮点数(32位)、双精度浮点数(64位)、延伸单精确度(43位以上,很少使用)和延伸双精确度(79位以上,通常以80位实现)。

打个比方说,IEEE754将一个单精度浮点数-110.101写成 -1.10101 * 210(注意这里的阶数10是二进制的2) 。转化后的数总共分为三个部分,数符、阶码和尾数。

数符

就是这个数的正负号,共占1位,1正0负。这里也就是1。

阶码

就是2的阶数(次方数),以移码表示,单精度浮点数共占8位,偏移量为127(28-1 - 1);双精度浮点数共占11位,偏移量为1023(211-1 - 1)。这里的10可以看成是0000 0010,转换成移码就是1000 0001。(简单的计算方法为:原码 + 偏移量 = 移码)

尾数

就是小数点后的数,以原码表示,单精度浮点数共占23位,双精度浮点数共占52位。这里是101 0100 0000 0000 0000 0000。

所以这个数在IEEE754的标准下表示为1 1000 0001 101 0100 0000 0000 0000 0000,写好看一点就是1100 0000 1101 0100 0000 0000 0000 0000。


隐含尾数最高位1

在IEEE754标准下的浮点数的尾数实际上隐藏了最高位1的。也就是说,如果尾数是0000…,那么它表示的其实是1.0000…。因此,做浮点数计算和转化的时候需要注意一下。


特殊浮点数

IEEE754规定了几个特殊的浮点数:

NaN,阶码全1,尾数非全0;
无穷大,阶码全1,尾数全0;
非规格化,阶码全0,尾数非全0;
零,阶码全0,尾数全0。

为什么IEEE754的偏移量要为127(28-1 - 1)、1023(211-1 - 1)这种数,原因之一就是为了能够规定这4种特殊的浮点数。


舍入规则

先说尾数的舍入规则。因为这是精度问题的关键原因之一!!!

以单精度浮点数来说:
1、当尾数第24位为0时,直接保留前23位,不进位;
2、当尾数第24位为1,且尾数24位之后的位数不全为0时,保留前23位,之后尾数第23位加1(会影响到整个数,导致整个数改变);
3、当尾数第24位为1,且尾数24位之后的位数全为0时,如果尾数第23位为0,则直接保留前23位,不进位;
4、当尾数第24位为1,且尾数24位之后的位数全为0时,如果尾数第23位为1,则保留前23位,之后尾数第23位加1(会影响到整个数,导致整个数改变)。

以上4条规则中,第2条和第4条是经尝导致数据出现无法解释的异常的罪魁祸首。


精度问题

实际上,9999.9995是不能精准转化为二进制的,因为0.9995不能用二进制小数来表示。
我们先把9999转化为二进制,即10 0111 0000 1111。整数部分一共14位,即规格化后需要占用尾数13位,也就是说作为一个单精度的float,还剩10位来表示它的小数部分。离0.9995最近的小数就是1111 1111 1101 1111 0011 1011…,这个数无限接近于0.9995。但实际上它只能保留10位,所以根据舍入规则10位以后的数就舍了。这样最后组成的float为0 1000 1100 001 1100 0011 1111 1111 1111,写好看点就是0100 0110 0001 1100 0011 1111 1111 1111。

而当数据为9999.9996时,离0.9996最近的小数是1111 1111 1110 0101 1100…,这个数无限接近于0.9996。根据舍入关系,这个是要进位的,也就是在尾数第23位上加1。0100 0110 0001 1100 0011 1111 1111 1111末尾加个1最终变成为0100 0110 0001 1100 0100 0000 0000 0000,而这个float写成整数形式就是二进制的10 0111 0001 0000也就是十进制的10000。


这也就解释了为什么:
Mathf.Floor(9999.9995f) = 9999
Mathf.Floor(9999.9996f) = 10000


再比如说,0.1f + 0.2f 不等于 0.3f。如果在C#中判断0.1f + 0.2f == 0.3f,编辑器会显示提示:Equality comparison of floating point numbers. Possible loss of precision while rounding values.
也就是说有可能会因为损失精度而产生未知的结果。


对于精度问题的解决方法,最简单的方式就是把单精度浮点数变为双精度浮点数。
在我们理解精度问题的底层原理之后,我们就可以有意识地避免精度问题发生。


总结

总的来说,基础知识十分重要。当遇到各种莫名其妙的问题时,挖底层原理永远是解决问题的好方法。


因为本人也还在学习过程中,所以肯定有很多地方考虑不全,以及会有很多错误。希望有大佬看到能够及时指出,我会立刻进行改正,帮助我自己和大家共同进步!

有问题的小伙伴们可以在下方留言,大家可以一起讨论。

  • 7
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值