IT干货网

浮点数计算精度丢失问题#W01

developer 2022年03月04日 编程设计 144 0

前几日电话面试,被问到一个问题: “浮点数计算精度丢失的原因是什么?”啊,啥? 我一脸懵逼,“就是在irb中,0.2+0.4不等于0.6,为什么?“,面试官又补了一句。我没有真正get到问题是什么,胡说了一通。

显然,这是问到了我的知识盲区了,我从来没有遇到过这个问题。于是我打开python命令行,执行了一下:

>>> 0.4 + 0.2
0.6000000000000001
>>>
我又打开Chrome,进入console

0.2 + 0.4

0.6000000000000001

一下子,我get到了这个问题,对于其原因,当然我还是懵逼的,于是我搜索了一下这个问题。浏览过了一些资料,这个问题极大的引起了我的兴趣,除了问题本身所涉及的知识,还有在这篇文章中,我被触动了,因为他说把理科当文科学了的人,我也在内。对于知识的学习,浮躁虚于表面,很多时候只记住了一个结论,但是底层的原因,却并没有去深入探知。对于学习的态度和方式,我也从头反思了一下,摒弃走马观花式的学习,用写作的方式将知识进行结构化,深入到细节。一个点一个点地学习知识,努力做到对于知识的表达能做到: 蔡崇信为什么敢冒这风险 这篇文章中所提到的:”优雅、简洁、有逻辑的表达“。

对于自己这方面的积累和刻意塑造,我想就从本篇文章开始,从这个有意思的问题开始:

为什么浮点数计算时会有精度丢失?

什么是浮点数?

浮点数是计算机表示有理数的一种方式,或者说规范。浮点数和定点数相对应。

什么是定点数呢? 这两个词中的‘点’也就是常说的小数点。定点数就是计算机在表示数字时,小数点的位置是固定的。

比如计算机用2字节二进制数来表示数字,它怎么表示呢? (这里只是演示概念)

2个字节有16位, 把前8位用来表示整数部分,后8位用来表示小数部分,也就是说小数点定在了第8位,当然具体定在多少位是可以设置的。用这种方式表示数字8.5时就是这样:

00001000.00001001
这样子,2字节的二进制只能表达2^8 =256个小数,而且范围有限(0 - 2^8),很多时候会浪费空间。有时候我的整数部分比较长,小数部分短,有时候反之。按照之前的方式,范围就会被限制,300.5 这种就没法被表示,需要4字节的空间来表示,那么这就很浪费了。

于是浮点数就出现了。

浮点数在表示的时候,小数点的位置可以根据实际情况移动。

Sign表示是正数还是负数,先不管。

Exponent表示指数部分,和 1.34*10^N 这个数中的N的性质一样,只是前面这个数的基数是10,它的基数是2 。这个部分的值会决定浮动的小数点到底定在哪个位置。

Mantissa就是实际的数字部分。

现在用浮点数来表示 5.2 ,整数部分是 101,小数部分是0.2,用二进制表示就是

0.2 * 2 = 0.4    0

0.4*2 = 0.8      0

0.8*2 = 1.6      1

0.6*2 = 1.2      1

0.2*2 = 0.4      0

······小数二进制换算方法

出来的结果就是 101.001100110011(这里是无限循环,暂时只截取一部分)

现在将上面这串数字填进上面那张图上:

Sign: 0   因为是正数

Exponent: Exponent表示指数N的值是多少,也就是 1.1232 * 2^N的这个N的值。这里要注意,因为N的值可为正,也可以为负。也就是说可以表达1.1232 * 2^5, 也可以表达1.1232 * 2^-5 这种数字,因为只有8个bit来表达Exponent, Exponent的取值范围就是-127~127, 要用8个bit表达这个范围,8个bit的范围是0~255,也就是 0~127 表示 -127~0, 127~255表示0~127 。要表示N=1,则需要加上127的基数。也就是说 N=1 时,Exponent为 10000001。举个反例,N=-1时,Exponent 为 00000001 。浮点数标准的表示范式是: 小数点的左边只有一个1,也就是说101.001100110011要表示成1.01001100110011 *2 ^2, 这里的N=2。它意味着这里小数点动态地定在了第三个数字这里。

Mantissa: 1.01001100110011001100110 (只有23个bit来表示)

所以填充之后的整体就是这样的:

0 10000010 01001100110011001100110  (共32位)

反向提取, 当看到上面这串二进制时,我知道这几个数据: 为正数; N=2,实际的浮点数是:1.01001100110011001100110 * 2^2

先说说十进制 

说完浮点数的概念,先来看看十进制。 123 在十进制中我们知道是100 + 20 + 3,每往左一位,就会在10的幂上加一,往右,会在10的幂上减一。

同样的在二进制中,0101 表示十进制中的5, 5=0*2^3 + 1*2^2 + 0*2^1 + 1*2^0 , 每往左一位,会在2的幂上加一。

十进制不能表达分数

十进制可以表达100,12345,但十进制能表达 1/3 吗?

答案是不能。 为什么? 因为1/3是无限循环小数,表达不了,当精度不够的时候,后面会被截断。

会发现只有当分母的因式分解中有2或者5,那么就可以表达成有限小数,也就是可能被10进制表达。

2*5=10 ,这是10进制的限制。

二进制不能表达大部分浮点数

看了上面十进制不能表达部分分数,就可以理解二进制不能表达很多浮点数了。除了表达范围的问题,很多小数在用二进制表达的时候会出现无限循环的情况。就像文章最上面的0.2,上面已经推导过,0.2得到的就是0.001100110011·····, 当后面的重复循环长度超过了计算机所能表达的范围时,它就会被截断。

也就是说

>>> 0.4 + 0.2
0.6000000000000001
>>>

0.4 和 0.2 在被计算机计算的时候,其值会被表达为近似值,精度在做运算之前就已经丢失了,结果肯定就变样了。

弥补方法

突然想起了,之前做支付接口的时候,会疑惑为什么要把人民币单位乘以100弄成分,而不是元。当时如果深入想想这个问题,也不至于现在才知道计算机领域这种最基础的常识了。

>>> 0.41 - 0.01
0.39999999999999997


原文链接:https://blog.csdn.net/u012671917/article/details/79669578


评论关闭
IT干货网

微信公众号号:IT虾米 (左侧二维码扫一扫)欢迎添加!

Java 开发者必备测试框架