美文网首页
求求你,不要再让浮点数背锅了

求求你,不要再让浮点数背锅了

作者: MakeItSimple | 来源:发表于2022-06-17 19:39 被阅读0次

缘起

今天刷技术公众号,看到了骇人听闻的标题: 踩坑了,BigDecimal使用不当,造成P0事故。点进去一看,影响到了核心链路上的收银台,P0不冤。
原文链接: https://juejin.cn/post/7087404273503305736

引出问题

作者写了示例代码,直接复现了问题。如下图所示。


问题复现

一句话,错误的使用了BigDecimal(double)的构造函数导致的。直接打开Double源码,源码作者也是用心良苦,直接警告了调用者,这玩意不准,大家要小心,还贴心地给了方法如何规避这个问题。如下图所示。


JDK中Double源码
写代码试了下,在阿里巴巴检查插件(Ali-Check)中直接提示该问题:
IDEA插件检查报错

心生疑问,为什么传入double就不准了呢?为什么会出现那么多位的不可描述的数字呢?复习下二进制小数的表达和存储就明白了。

浮点数详解

1、数学上二进制小数的表达

大家都知道,十进制数字的表达式:d = \sum^{m}_{i = -n}{10^i \times d_i}
其中,m为整数部分的位数,n为小数部分的位数;di是0~9的整数。
比如:1230.56 = 1\cdot10^3 + 2\cdot10^2 + 3\cdot10^1 + 0\cdot10^0 + 5\cdot10^{-1} + 6\cdot10^{-2}

以此类推,二进制数字的表达式:d = \sum^{m}_{i = -n}{2^i \times d_i}
其中,m为整数部分的位数,n为小数部分的位数;di是0~1的整数。
比如:10.01_2 = 1\cdot2^1 + 0\cdot2^0 + 0\cdot2^{-1} + 1\cdot2^{-2}

知道了这个,那问大家一个问题。0.1用二进制中如何表达呢?套用上面的求和表达式,似乎要先变成d\cdot2^i(d=0,1)的求和形式才行。
教大家一个方法,就是不停的乘以二进制的2(类似于不停的用10乘以10进制下的小数),以得到小数点后一位上的值。

推倒过程分解:
第1位:0.1\cdot2=0.2 --> 乘积小于1,故为0 --> 得到小数0.0
第2位:0.2\cdot2=0.4 --> 乘积小于1,故为0 --> 得到小数0.00
第3位:0.4\cdot2=0.8 --> 乘积小于1,故为0 --> 得到小数0.000
第4位:0.8\cdot2=1.6 --> 乘积大于1,故为1 --> 得到小数0.0001 (注意,这里要减去1后重新来过)
第5位:0.6\cdot2=1.2 --> 乘积大于1,故为1 --> 得到小数0.00011
第6位:0.2\cdot2=0.4 --> 乘积小于1,故为0 --> 得到小数0.000110(大家发现没有,到这里其实跟第2位的计算一样,开始无限循环了)
故最终的二进制表达式为:0.00011[0011](后面的中括号中无限循环)

惊不惊喜,意不意外,0.1这么简单的数字,在二进制中居然无限循环。如果再复杂点的数字,也就更加不可预测了。
当然,也有满足条件的小数是能被二进制准确(有限位内)表示的。条件就是它能够被表示成d = \sum^{m}_{i = -n}{2^i \times d_i}的形式。比如:63/64,26/128。发现没?都是2的幂为底的分数。在实数的世界里,沧海一粟。

数学上可以有无限循环的概念去表达一个小数,但是计算机的存储长度是有限制的(float是32位,double是64位),也就无法准确表达这个0.1了,即使是能够准确表达的小数,如果位数太长,由于计算机位数的限制,也是会被截断的。

可以想象,在浮点数的计算过程中,势必会有截断(向上舍入、向下舍入、向偶舍入、向零舍入)的操作,那计算结果自然会存在精度的问题了。现在再回想下,计算机计算出上面的结果:8.8000000000000000000007105427......也就不奇怪了。再把这个值直接传给BigDecimal(用double入参的方式),那BigDecimal也表示很无奈。

试试看 动动小手,看看63/64和26/128表示成二进制该如何写?

2、计算机中浮点数的存储

计算机中使用的是IEEE浮点标准,这个标准统一了不同机器上的浮点数存储标准。下面说说这个详细的存储方法。
IEEE浮点标准用V=(-1)^s\times M \times 2^E的形式来表示一个数。

  • 符号s(sign)决定是正数(s=0)还是负数(s=1);
  • 有效数M(mantissa)是一个二进制小数,它的范围在[1,2)或者[0,1)之间;
  • 指数E(exponent)是2的幂(可能是正数,也可能是负数);

下图表示了float和double的存储格式:


浮点数的存储示意图

按照E的不同取值,分为三种情况。
1、格式化值
E既不是全是0,也不是全是1时,属于格式化的值。此时exponent的位数中不再有符号位,也就是无法天然地表达正负数。于是就引入了偏置(biased)的概念。也就是说E=e-biased(这里e表示exponent区域的二进制表达的实际数字),而真正的指数值E需要减去偏置(biased)。对于float,biased=127;对于double,biased=1023。

对于有效数字M,其取值范围是[1,2)。由于二进制的小数表达中,第一位一定是1,比如正常的二进制科学计数表达式:1.01000101 \times 2^6。为了节省这一位,这个1也就不会保存在M中。换言之,其存储的是小数点以后的二进制数,隐藏了1。

2、非格式化值
E全是0时,表示非格式化的值。此时,E=1-biased;同时M的范围变成了[0,1),也不会再隐藏1。

非格式化值的2个作用如下所述:

  • 表示0(有意思的是IEEE标准中,既有+0,也有-0,且二者不等)
    float的+0的格式:0 00000000 00000000000000000000000
    float的-0的格式:1 00000000 00000000000000000000000
    double一样,这里不赘述。
  • 表示非常接近0的数
    这个非常近怎么衡量呢?对于float来说,就是小于2^{-127};对于double来说,就是小于2^{-1023};因为如果数字比这个还小,那么E由于存储位数的限制是无法表达的。

3、特殊值
E全是1时表示特殊值。

  • 正无穷: s=0,E全是1,M全是0;
  • 负无穷: s=1,E全是1,M全是0;
  • NaN(Not a Number):E全是1,M不为0;

举个栗子
float f = 0.1f; int ifv = Float.floatToIntBits(f); System.out.println(Integer.toBinaryString(ifv));
此时输出:
111101110011001100110011001101

补足32位,并按照1、8、23来划分开,分别计算s、E和M。
0 01111011 10011001100110011001101
s=0
E=123-127=-4
M=1.10011001100110011001101 \times 2^{-4} = 0.000110011001100110011001101

我们上面计算过0.1的二进制表达:0.00011[0011](后面的中括号中无限循环)
看吧,对上了,明显计算机做了截断。

回顾

今天我们学习了2个内容:
1、数学上小数的二进制表达方法。
2、计算机中二进制浮点数的保存标准。

结论

  • 计算机世界中二进制并不能精确表达所有浮点数;浮点数计算存在精度丢失。
  • 科学计算是可以用浮点数运算;金融计算用BigDecimal。

相关文章

  • 求求你,不要再让浮点数背锅了

    缘起 今天刷技术公众号,看到了骇人听闻的标题: 踩坑了,BigDecimal使用不当,造成P0事故。点进去一看,影...

  • 不要再让原生家庭背锅!

    从小时候,爸爸妈妈就是三天一大吵,两天一小吵,可能有时候因为婆媳关系爸爸会对妈妈大打出手,我就是在这种环境下长大,...

  • 请不要再让“原生家庭”来背锅了!

    近几年,随着人们对精神层次的追求越来越高和现实对心理疏导的需求越来越大。很多我们以前没有听到过的关于身心灵成长方面...

  • 五月12星座运势

    经历了三月的水星逆行,木星逆行,土星逆行,水星逆行已经结束,水星也已经完全恢复良好的状态,所以不要再让水逆无辜背锅...

  • 外墙裂缝,别再让“真石漆”背锅了!

    外墙真石漆的喷涂施工是一个技术活,所有的标杆的外墙真石漆工程全都是凭借精工细作完成的。正所谓细节决定成败,真石漆施...

  • 已经没有心力

    我已经没有心力再走下去 我求求你了 不要再让我走下去了 人间已无法继续 每一天都是在拖延 我求求你了 我要不要放下...

  • 搭个平台也不是那么好弄的

    割韭菜还是最好的办法,没有比这更好的方法了。 锅总是要有人背的。 努力!不要成为背锅侠。

  • 老领导调走了,答应我的事咋办呢?

    昨天说到领导让小贾背锅,小贾在犹豫要不要替领导“顶雷”。 后台有位网友留言,说自己选择了给领导背锅,结果背锅之后,...

  • 赵本山求你了,不要再让球球拍电影了

    从上次赵本山女儿球球捐款270万。 果不其然发现了球球主演的一部电影。 《武神苏乞儿之黄金海盗》名字倒是大气。 怎...

  • 求求你不要再让我淘宝点赞了

    (温馨提示:此文属于吐槽,阅读前请自行做好心理准备。) ✨ 01 就在刚才,手机屏幕亮起,打开一看,三个人发信息让...

网友评论

      本文标题:求求你,不要再让浮点数背锅了

      本文链接:https://www.haomeiwen.com/subject/uhttvrtx.html