浮点数解惑


注:文章中涉及的运算的结果,通过http://babbage.cs.qc.edu/IEEE-754/Decimal.html进行验证

在我们编程的时候经常会涉及到浮点数。 而且你会奇怪的发现 0.1+0.2==0.3的逻辑值为false,你知道这是为什么么?

使用python直接计算得到的结果是0.30000000000000004 这其实涉及到浮点数在计算机中的表示方法,舍入的方式,以及如何进行数学运算等问题。

一.十进制转二进制

首先看下十进制的浮点数如何通过二进制的方式来表示. 比如3.6如果使用二进制的方式来进行表示应该如何做呢? 我们可以将整数和小数分开进行。 对于整数部分我们可以使用“除二取余,倒序排列”法来进行。

3除以2 上1余1
1除以2 上0余1
所以3使用二进制表示就是11

小数部分“乘二取整,顺序排列”法来进行

0.6乘以2 得1.2 1
0.2乘以2 得0.4 0
0.4乘以2 得0.8 0
0.8乘以2 得1.6 1
0.6乘以2 得1.2 1 从这里开始1001循环

所以3.6使用二进制进行表示的话就是11.100110011001….(1001循环)

二.IEEE754标准

1.IEEE 754 存储格式

在计算机里表示一个数字可没有像我们在纸上写上一个阿拉伯数字那么简单。 目前的计算机能够认识的就是0101,所以我们的数得转换成二进制表示,但同时数有正负,所以一般有一个符号位的概念。 那么浮点数呢?我们知道浮点数可以使用科学计数法来进行表示(在科学记数法中,一个数被写成一个1与10之间的实数(尾数)与一个10的幂(指数)的积,为了得到统一的表达方式,该尾数并不包括10)。 类似,二进制的浮点数也可以写成类似十进制的科学计数的形式,只不过底数不是10,而是2了。 比如 0.00101可以表示成1.01X2^(-3)
现在我们来正式谈谈IEEE 754标准了,

一个按照IEEE 754标准表示的浮点数一般由一位符号位,m位指数(注意,实际存储的是真实指数值加上2^m-1 之后的值,因为为了方便指数的对其比较,不加的话,指数有正有负比较起来不方便),n位尾数组成(由于尾数在存储的时候其整数位上都规定为1,所以为了节省空间,这个整数位上的1就省去了),且高位在左低位在右.

常见的C或者Java语言,都实现了IEEE 754的浮点数标准。 比如C或者Java中的float和double分别对应单精度浮点数和双精度浮点数,这两类表示的方式相同,不同的只是指数和尾数的位数不一样而已。 单精度浮点数由32位二进制位组成(四个字节),其中1位符号位,8位指数位,23位尾数位。

双精度浮点数由64位二进制位组成(八个字节),其中1位符号位,11位指数位,52位尾数位。

2.舍入规则

IEEE 754-1985 中提及了四种舍入规则

  • 舍入到最接近,在一样接近的情况下偶数优先(Ties To Even)(这是默认的舍入方式):会将结果舍入为最接近且可以表示的值,但是当存在两个数一样接近的时候,则取其中的偶数(在二进制中式以0结尾的)
  • 向+∞方向舍入:会将结果朝正无限大的方向舍入。
  • 向-∞方向舍入: 会将结果朝负无限大的方向舍入。
  • 向0方向舍入: 会将结果朝0的方向舍入。

对于第一种舍入规则,有必要详细的说明下。 在十进制中比如0.45 按照第一个规则保留小数点后面一位小数进行舍入的话,就应该为0.4 因为0.45和0.4和0.5的距离一样,但是4为偶数,所以取0.4. (对于这部分你可以在大脑中想象出一根数轴来)
对于二进制数来说也一样,比如有这样的一个二进制表示的数 0.1011
如果想保留小数点后面一位,发现后一位之后为011,很显然0.1011距离0.1近,距离1.0远,所以后面的011果断舍去得到0.1
但是如果需要保留小数点后面两位,发现两位之后是11,很显然0.1011距离0.11更近,所以果断进位,得到0.11
如果保留小数点后3位,这个时候问题来了,因为0.1011和0.101,0.110的距离相等,但是由于0.110末位为偶,所以得到0.110

三.问题解析

现在我们来看看开始说的0.1+0.2为什么不等于0.3的问题 在Python中浮点数是64位双精度浮点数,相当于C语言中的double

0.1在计算机中如何表示的呢?

0.1X2=0.2 0
0.2X2=0.4 0
0.4X2=0.8 0
0.8X2=1.6 1
0.6X2=1.2 1
0.2X2=0.4 0 开始0011循环

所以0.1的二进制为
0.00011001100110011001100110011….
现在将其表示为IEEE 754 浮点数的形式
0.1是正数,符号位为0
0.00011001100110011…=1.100110011001…X2^(-4)
所以其指数是-4,加上1023就是1019,1019的二进制值是01111111011
剩下的就是尾数部门了,去除首尾的整数部分,末尾待舍去的是1001…所以需要向前进一位
0011111110111001100110011001100110011001100110011001100110011010
同理可以得到0.2的IEEE 754的格式为(其中指数为-3,填入的时候是-3+1023=1020)
0011111111001001100110011001100110011001100110011001100110011010
两个数相加,这个时候发现指数不一致,所以需要对齐,一般情况下是向右移,因为最右边的即使溢出了,损失的精度远远小于左边溢出。
所以这个时候指数的真正的值都为-3,然后0.1右移后的尾数与0.2的尾数进行相加(别忘记首位的1)
这个时候尾数相加

 0.1100110011001100110011001100110011001100110011001101  
+1.1001100110011001100110011001100110011001100110011010
--------------------------------------------------------
10.0110011001100110011001100110011001100110011001100111
规格化之后(注意末尾有进位)
为1.0011001100110011001100110011001100110011001100110100X2^(-2)  

0011111111010011001100110011001100110011001100110011001100110100
其化成十六进制的表示为3FD3333333333334
通过在线的求值工具http://babbage.cs.qc.edu/IEEE-754/64bit.html可以求得,其值为0.30000000000000004
而0.3在计算机中的格式为
0011111111010011001100110011001100110011001100110011001100110011
化成十进制为
0.29999999999999999
所以说造成0.1+0.2!=0.3的根本原因在于舍入上。0.1和0.2在舍入的时候都是进位,而0.3没有进位,所以0.3<0.1+0.2

四.其他

如果当我们数值计算的精度超出了IEEE 754所能表达的范围的时候,我们应该怎么办呢?为了满足这个需求,一般在高级语言中都有软件的方式实现,比如Java中的BigDecimal。
在经济、金融和与人相关的程序中,建议使用十进制浮点数。但是,由于没有硬件支持,用软件实现的十进制浮点计算比硬件实现的二进制浮点计算要慢。不过IBM公司貌似有硬件上的支持https://www.ibm.com/developerworks/wikis/display/WikiPtype/Decimal+Floating+Point

参考资料: http://en.wikipedia.org/wiki/IEEE_754-1985 http://babbage.cs.qc.edu/IEEE-754/References.xhtml http://babbage.cs.qc.edu/IEEE-754/ http://www.ruanyifeng.com/blog/2010/06/ieee_floating-point_representation.html

卢克 /
Published under (CC) BY-NC-SA in categories Programming  tagged with IEEE  Number