浮点数的这些坑,你未必知道-深入理解浮点数的规律

我猜作为开发工程师,大部分人都用过浮点数。但是你是否用对了呢?你是否知道,浮点数有近一半的值,在-1与+1以内呢?
 
本节大纲有:
1、基本数据类型
2、认识浮点数规律
3、我在浮点数上踩过的坑
 
在计算机的眼中,一切都是数字,一切都是二进制。

一、基本数据类型

关于数值,你应该时刻牢记在心里的三点,
1、每个基本类型的数值都是有范围限制的,不是无限大的。
2、无论是boolean,int,float,string,struct,object,最终都对应计算机的一个byte或者多个byte。
3、非必需,不要使用浮点数。
 
下面说几个在工作中会经常遇到的几个问题:
1、计算机中32位的有符号整型int,其最大值就是2147483647,这个数大约是21亿多。如果放普通计算应该都不成问题。但是,如果你数据库表的主键是int类型,那你就要小心了,好多大厂都在这个数据类型上面栽过跟头。因为现在中国网民基数太大,一张表很容易上亿,一旦超过21亿多,你的系统可能会因为db主键溢出导致故障。
2、在金融行业应该注意数值溢出的问题。两个integer相加或者相乘,其结果值很有可能超过integer的表示范围。在金融这种对精确度要求特别高的行业,这种错误更是不可忍受的。
3、返回给前端的json数据,尽量不要使用数值类型。如果你做过前端相关的项目,你会发现,当你使用json格式字符串给前端返回数值类型时,前端展示出来的数字可能跟你返回的数字完全不一样。其根本原因就是Javascript的浮点数没有办法精确表示较大数值。
 
 
 

二、认识浮点数规律

如果你没有耐心去深入理解浮点数(说句实话,确实复杂),那么我建议你至少应该记住关于浮点数的几个规律和建议。
  1. Javascript中的数字都是双精度浮点数。
  2. 在浮点数中,0有+0和-0两种表示方法
  3. 浮点数表示的值是不连续的,不均匀的。越大的数,越无法用浮点数表示出来。
  4. 有近一半的浮点数数值分布在-1和+1之间。
  5. 不要用浮点数来生成随机数。
  6. 不要用浮点数来存储或计算有关金钱和资产方面的一切,包括常见的会员积分。不能用浮点数用于任何需要精确值的运算,因为浮点数无法精确的表示数字,比如说0.1就无法用浮点数精确表示出来。
规律论证:
有关浮点数详细教程,大家可以自己百科(IEEE754标准)。32位的浮点数长度数值太多,不好理解,我们用8bit长的浮点数来学习,学习得出的规律,对32位和64位浮点数同样适用。
8bit浮点数,定义阶码E为4bit,尾数M为3bit,符号位1位。
8bit的浮点数,其能表示的数值只有256个,全部列出来仍然很多,我们把其中不太重要的数值省略掉。
8位浮点数256种状态表示值如下(下图出自Computer Systems: A Programmer's Perspective)。

 图一

上图列出的是正浮点数,即只列出了0及以上127个数值部分。
其中第二列为二进制表示。从上至下是连续增长的二进制,一共有128-8=120个状态值(01111000之后有8个状态值浮点数没有使用),但是因为是浮点数,所以其表示的值,需要经过一定计算规则才能得出,最后一列就是二进制对应十进制值。
8位浮点数,其所表示的数值范围是0-240。尽管其二进制只有120个状态值,但是其表示的最大值却达到了240,所以在240以内,一定有很多整数值,无法用8位浮点数表示。所以说,浮点数表示的值,不是连续的。如果将其能够表现出来的值,使用蓝点画出来,其所表示的数值大概如下:

 图二

图出自Computer Systems: A Programmer's Perspective。
从上图可以看出,浮点数表示的数值,靠近0中间最多,越靠外(数值越大),浮点数数值越少。比如从图一的表中可以看出,数值224-240之间的整数,都没有办法用8位的浮点数表示出来。所以说,越大的数,其用浮点数表示的概率越低。
另外,如果你利用浮点数小数乘以一个倍数来生成整数随机数,你会发现,你生成的随机数出现跟浮点数一样的规律,即生成的随机数大多数都集中靠近中心0的位置。所以,不要利用浮点数生成随机数。
 
我们放大-1至+1范围的数值

 图三

我们发现在1以内,像0.1,0.2等小数,也是没有办法用8位浮点数表示出来的。从这里也可以看出,浮点数的0有+0和-0之分。
我们使用8位浮点数和32位浮点,计算一下1以内和1以外的规格化数字的大概个数。因为64位太大,所以我这里就不再计算了,但是其数值个数比例应该是类似的。
浮点数
8位浮点数
32位浮点数
1的二进制
 0 0111 000
0 0111 1111 000 0000 0000 0000 0000 0000
1以内数值个数
 56
1,065,353,216
最大规格数二进制
 0 1110 111
0 1111 1110 111 1111 1111 1111 1111 1111
最大规格数表示的数值
 119
 
1以上二进制个数
 119-56= 63
2,139,095,039 -1,065,353,216=
1,073,741,823
从以上数值计算可以看出,有近一半的浮点数值,在-1和1以内。
 

三、我在浮点数上踩过的坑

作为亲身经历者,我说两个案例。
第一个案例,用户返现金额是前端JS计算。我上面说过了,JS中的数值都是双精度浮点数,正好返现中用到了减法,于是就出现计算出的返现结果是1.199999999结果,然后直接传给了后端。但是钱的精度是不能超过2位小数的,后端直接将后面的9给截掉了,于是就返给客人的钱就少了1分钱。当然,这都是极少的情况,所以问题隐藏了很久。
第二个案例,后端用double类型来计算退款金额,当一笔交易多次退款的时候,有时出现钱不够退或者没有退完的情况。经过多次用户投诉,最终才修复这个问题;
 
浮点数使用不当的问题,通常情况很难发现。但是只要存在,就一定会出现,现在没有遇见,不代表将来也没有。迄今为止,为浮点数交的学费最贵的当属欧洲航空总署,因为浮点数转换不当,导致在1996年6月4号发射阿丽安娜5火箭爆炸,当时的火箭里面放着一个价值5亿美金的卫星。
国内也有好多一些工程师将遇见的问题分享了出来,随便百度了一个浮点数踩坑案例,https://blog.csdn.net/yangsh3002/article/details/52507097
 
如果你有时间,你可以运行如下Java代码,看看效果
public class FloatTest {
    public static void main(String[] args) {
    double a = 1.0/10;
    double b = 1-0.9;
    System.out.println("1.0/10="+a);
    System.out.println("1-0.9="+b);
    System.out.println(a==b);
    }
}

 

在我的电脑上输出如下
1.0/10=0.1
1-0.9=0.09999999999999998
false

 

我的所有文章,会第一时间发布在我的公众号中。

 

 

相关阅读:

 
 
 
 
参考资料:

posted @ 2021-11-04 21:23  猿界汪汪队  阅读(1923)  评论(0编辑  收藏  举报