浮点数精度问题原因以及各种现象的解释

在知乎上看到有人提出类似的问题,我总结表述一下,做一下笔记:

一步步解释:

存储问题

首先要了解,从根本上讲,计算机只识别二进制数,无论我们使用何种编程语言,在何种编译环境下工作,都需要将程序编译成二进制机器码才能让计算机执行
那么这样会导致什么问题呢?会导致小数的十进制转为二进制出现误差,这就是所谓的精度缺失。

为什么会出现误差?这就涉及到了计算机原理的问题:

我们知道数字在计算机中是以二进制存储的。对于整数来说,只要不超过整数的表示范围,一定都可以表示成二进制的形式,比如8是1000,88是1011000,可是小数小数部分就没有那么幸运了,根据二进制小数转换成十进制的规则(把该小数不断乘2,再取所得的整数部份,直至没有小数或足够长度为止)(二进制整数换成十进制规则 除二取余倒记发)。

由于二进制中所有的小数存储的位数是有限,因此我们得知“任何十进制整数都可以精确转换成一个二进制整数,但任何十进制小数却不一定能精确转换成一个二进制小数,只要转换过程中乘积的小数部分满足所需精度即可”。比如对于0.1来说就不能精确转换为一个二进制小数,在16位小数的限制条件下,离它最近的二进制小数是0.0001100110011,也就是十进制的0.0999755859375。所以虽然你写程序的时候写的是0.1开始这个数存储到b这个float变量里的时候就变成了0.0001100110011,也就是0.0999755859375,因此你输出它的时候就会出现精度误差了,这种误差是不可避免的。
这里我有个疑问,为什么我们不把小数也当做整数存储呢?我怀疑是因为存储方式的问题,然后我找到了原码、反码、补码的文章,然后我又想到为什么要用补码?然后找到了加法器,然后就想为什么只有加法器?到这感觉有点不清楚了,我要好好找一下,有情况再回来更新。

再来,根据IEEE754(二进制浮点数算术标准) 来说:

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

(2)2^E表示指数位。

(3)M表示有效数字,大于等于1,小于2。

IEEE 754规定,有四种精度的浮点数:单精确度(32位)、双精确度(64位)、延伸单精确度(43比特以上,很少使用)与延伸双精确度(79比特以上,通常以80位实现)。
对于32位的浮点数,最高的1位是符号位s,接着的8位是指数E,剩下的23位为有效数字M。

32位浮点数存储格式

对于64位的浮点数,最高的1位是符号位S,接着的11位是指数E,剩下的52位为有效数字M。

64位浮点数存储格式

其实看看上图你就应该明白,还有一种精度丢失就是长度丢失(不同类型存储结构不同),但那种比较简单,就不做讨论。

继续来讲,上图到底是什么意思呢。咱们先慢慢来讲:

正规化(规约形式)

如果浮点数中指数部分的编码值在[图片上传失败...(image-c806de-1554705679571)]之间,且在科学表示法的表示方式下,分数 (fraction) 部分最高有效位(即整数字)是1,那么这个浮点数将被称为规约形式的浮点数。
如上存储格式中所示:

  1. 首先对于第一段sign,就是符号位,0代表正,1代表负。。

  2. 第二段exponent指数位,实际也是有正负的,但是没有单独的符号位,在计算机的世界里,进位都是二进制的,指数表示的也是2的N次幂,8位指数表达的范围是0到255,而对应的实际的指数是-127到128。也就是说实际的指数等于指数位表示的数值减127。这里特殊说明,-127和+128这两个指数数值在IEEE当中是保留的用作多种用途的,
    关于指数位与实际的指数之差叫做移码,移码的值

精度 M(阶/指数数位) 移码 二进制表示
单精度 8 127 0111 1111
双精度 11 1023 011 1111 1111
长双精度 15 16383 011 1111 1111 1111
  1. 第三段fraction尾数位(也叫有效数字),只代表了二进制的小数点后的部分,小数点前的那位被省略了,当指数位全部为0时省略的是0否则省略的是1。

举个栗子(以32位浮点数为例,这样误差比较大):

比如有个浮点数17.35需要保存。
17.35转为二进制:17转为10001(这就不用说了吧)
0.35呢?(乘2取整法)
0.35 * 2 = 0.7 记为 0
0.70 * 2 = 1.4 记为 1
0.40 * 2 = 0.8 记为 0
0.80 * 2 = 1.6 记为 1
.......这么算有结束吗?当然没有...所以就按照能保存的极限位数保存相对精度。  

这样我们17.35就变成了10001.0101100110011001100共预留24位数  
然后把这串数字右移至小数点前只剩1位:  
1.00010101100110011001100---》 右移了4位  
这样得到了指数位和尾数位:  
指数:实际为4,必须加上127(转出的时候,减去127),所以为131。也就是10000011。  
尾数:00010101100100100100100 (1就不用保存了)。  

到此,十进制浮点数就变成了二进制浮点数存储了。

非正规化:0的表示(非规约形式)(-127)

如果浮点数的指数部分的编码值是0,分数部分非零,那么这个浮点数将被称为非规约形式的浮点数。分数部分全为0,就是表示浮点数0

比如说我们要存储0.35这个浮点数,按上述正规化的形式就无法存储,因为不知道指数位写多少,于是就有了约定,约定指数位为0就表明非正规化。
所以,当见到指数部分为0是,尾数部分就不再是1.bbbbb...而是0.bbbbb...了。

无穷大与NAN

上面说了,指数位预留了两个值(-127和128),-127是做非规约形式的,那么128呢?

当指数位达到当前浮点数最大指数值时:

  • 尾数位全为0就表示无穷大。
  • 尾数位不全为0就表示NaN。

舍入规则

还是以32位单精度浮点数为例。
32位单精度浮点数存储23位尾数位,那么就以第24位位判断依据

  • 如果 24位为1 24位之后都是0 :如果23位为0,则舍去不管;如果23位为1,则24位向23位进1,使23位还是0。
  • 如果 24位为1 24位之后不全0 :24位向23位进1。
  • 如果 24位为0 舍

** 因为位数、算法、规则之类的约定,得知计算机浮点数并不是严格存储这就是为什么浮点数运算出现问题的原因 **


有事情要做,先写到这。
待说明问题:

  1. 关于渐进式下溢出的理论与由来。
  2. 关于以下代码出现情况的说明:
System.out.println(0.1 * 1);
System.out.println(0.1 * 2);
System.out.println(0.1 * 3);
System.out.println(0.1 * 4);
System.out.println(0.1 * 5);
System.out.println(0.1 * 6);
System.out.println(0.1 * 7);
System.out.println(0.1 * 8);
System.out.println(0.1 * 9);
System.out.println("=====================");
System.out.println(0.0999999999999999999999999999999999999999999999999);
double f1 = 9.7;
double f2 = 0.7;
double f3 = 9;
System.out.println(f1-f2-f3);
System.out.println(f1-f3-f2);

输出结果:
0.1
0.2
0.30000000000000004
0.4
0.5
0.6000000000000001
0.7000000000000001
0.8
0.9
=====================
0.1
-6.661338147750939E-16

问题:

  1. 为什么有的输出正确,有的不行
  2. 为什么0.0999--的输出为0.1
  3. 为什么f1-f2-f3得到0.0,而f1-f3-f2却得到那样的结果

这些问题我现在也并没有思考清楚,等待后续思考。
当然,有问题可以在评论中讨论指出。

你可能感兴趣的:(浮点数精度问题原因以及各种现象的解释)