最近经常碰到浮点数相减的问题,本来按照原先的预期,两个数字相减后的结果是1的,最后返回显示是0.999999999999。 一直搞不清楚原因,这次正好来整理一下。下面我们先以一段php代码开始。
<?php var_dump(0.1 + 0.2 == 0.3); $# php test.php bool(false)
大家一看结果是false,顿时会有一个疑问:这是怎么回事,这是php的bug吗?其实这不是,很多语言都有这个问题,要想搞清楚这问题怎么产生的,我们首先要知道浮点数是怎么表示的。
理解浮点数的第一步是考虑含有小数的二进制数字是怎么表示的,要想理解二进制,我们先看下我们熟悉的十进制表示法。
12.34 = 1*102 + 2*101 + 3*10-1 + 4*10-2 = 10 + 2 + 0.3 + 0.04
数字权的定义与十进制的小数点(.)有关,这意味着小数点左边的数字的权值是10的正幂,得到整数值,而小数点右边的数字的权是10的负幂,得到小数值。
那如果换成二进制的话
101.11 = 1*22 + 0*21 + 1*20 + 1*2-1 + 1*2-2 = 4 + 0 + 1 + 0.5 + 0.25 = 5.75
假定我们仅考虑有限的编码,那么十进制表示法不能准确的表达1/3和5/7这样的数字,类似,小数的二进制表示法也有这个问题,有些小数只能够被近似的地表示,但增加二进制表示的长度可以提高表示的精度。
十进制与二进制的转换
十进制转换为二进制,分为整数部分和小数部分。
1:整数部分
方法:除2取余法,即每次将整数部分除以2,余数为该位权上的数,而商继续除以2,余数又为上一个位权上的数,这个步骤一直持续下去,直到商为0为止。
最后读数时候,从最后一个余数读起,一直到最前面的一个余数。
例子:将十进制的168转换为二进制 得出结果 将十进制的168转换为二进制,(10101000)
分析:
- 第一步,将168除以2,商84,余数为0。
- 第二步,将商84除以2,商42余数为0。
- 第三步,将商42除以2,商21余数为0。
- 第四步,将商21除以2,商10余数为1。
- 第五步,将商10除以2,商5余数为0。
- 第六步,将商5除以2,商2余数为1。
- 第七步,将商2除以2,商1余数为0。
- 第八步,将商1除以2,商0余数为1
- 第九步,读数,因为最后一位是经过多次除以2才得到的,因此它是最高位,读数字从最后的余数向前读,即10101000
2:小数部分
方法:乘2取整法,即将小数部分乘以2,然后取整数部分,剩下的小数部分继续乘以2,然后取整数部分,剩下的小数部分又乘以2,一直取到小数部分为零为止。
如果永远不能为零,就同十进制数的四舍五入一样,按照要求保留多少位小数时,就根据后面一位是0还是1,取舍,如果是零,舍掉,如果是1,向入一位。换句话说就是0舍1入。
读数要从前面的整数读到后面的整数。
例子:将0.125换算为二进制 得出结果:将0.125换算为二进制(0.001)
分析:
- 第一步,将0.125乘以2,得0.25,则整数部分为0,小数部分为0.25。
- 第二步, 将小数部分0.25乘以2,得0.5,则整数部分为0,小数部分为0.5。
- 第三步, 将小数部分0.5乘以2,得1.0,则整数部分为1,小数部分为0.0。
- 第四步,读数,从第一位读起,读到最后一位,即为0.001。
我们可以按照上面的公式来分析一下0.1和0.2,这时候大家会发现,根本就没办法小数部分为零,就跟10/3一样是除不尽的。
针对这种情况,我们只能根据具体情况来决定保留多少位小数点,保留的位数越多,数字就越精确。
就像10/3一样,我们保留两位小数那就是3.33,保留四位小数那就是3.3333,同样是乘以3,那么3.3333*3的值肯定比3.33*3的值更靠近10。
其实到这一步,我们就大概知道了为什么0.1+0.2!=0.3的原因了,因为0.1和0.2转换为2进制的时候丢失了部分精度,那当然不可能等于0.3了。我们可以拿两个不丢失精度的小数来相加。例如0.5+0.25,那肯定是等于0.75的。
下面我们来介绍一下浮点数。
浮点数
IEEE二进制浮点数算术标准(IEEE 754)是20世纪80年代以来最广泛使用的浮点数运算标准,为许多CPU与浮点运算器所采用。
这个标准定义了表示浮点数的格式(包括负零-0)与反常值(denormal number),一些特殊数值((无穷(Inf)与非数值(NaN)),以及这些数值的“浮点数运算符”;它也指明了四种数值舍入规则和五种例外状况(包括例外发生的时机与处理方式)。
计算机使用浮点数运算的主因,在于电脑使用二进位制的运算。例如:4÷2=2,4=100(2)、2=010(2),在二进制相当于退一位数。则1.0÷2=0.5=0.1(2)也就是。依此类推二进制的0.01(2)就是十进制==0.25。
由于十进位制无法准确换算成二进位制的部分小数,如0.1,因此只能使用近似值的方式表达。
一个浮点数 (Value) 的表示其实可以这样表示:
也就是浮点数的实际值,等于符号位(sign bit)乘以指数偏移值(exponent bias)再乘以分数值(fraction)。
我们常见的浮点数类型分为32位单精度和64位双精度,那么我们介绍下。
32位单精度
单精度二进制小数,使用32个比特存储。
S为符号位,Exp为指数字,Fraction为有效数字。
指数部分即使用所谓的偏正值形式表示,偏正值为实际的指数大小与一个固定值(32位的情况是127)的和。
采用这种方式表示的目的是简化比较。因为,指数的值可能为正也可能为负,如果采用补码表示的话,全体符号位S和Exp自身的符号位将导致不能简单的进行大小比较。
正因为如此,指数部分通常采用一个无符号的正数值存储。单精度的指数部分是−126~+127加上偏移值127,指数值的大小从1~254(0和255是特殊值)。
浮点小数计算时,指数值减去偏正值将是实际的指数大小。
64位双精度
双精度二进制小数,使用64个比特存储。
S为符号位,Exp为指数字,Fraction为有效数字。
指数部分即使用所谓的偏正值形式表示,偏正值为实际的指数大小与一个固定值(64位的情况是1023)的和。
采用这种方式表示的目的是简化比较。因为,指数的值可能为正也可能为负,如果采用补码表示的话,全体符号位S和Exp自身的符号位将导致不能简单的进行大小比较。
正因为如此,指数部分通常采用一个无符号的正数值存储。双精度的指数部分是−1022~+1023加上1023,指数值的大小从1~2046(0(2进位全为0)和2047(2进位全为1)是特殊值)。
浮点小数计算时,指数值减去偏正值将是实际的指数大小。
提示:
解决方案:直接将小数变为整数,然后再比较,例如0.1*10+0.2*10==0.3*10,这种比较返回的肯定是true。
注意:很多人在使用mysql的时候,会使用float来存储价格,例如123.45,单位是元,这种后期做算法都是有概率出现问题的,我的建议就是使用int,单位为分。
参考链接:
《深入理解计算机系统》
https://zh.wikipedia.org/wiki/IEEE_754#32%E4%BD%8D%E5%96%AE%E7%B2%BE%E5%BA%A6