你真的知道0.1+0.2为什么不等于0.3吗?

打开chrome控制台,给一个特别简单的输入以下:python

0.1 + 0.2 // 0.30000000000000004 复制代码

不知道你有没有吃惊,这么简单的一个计算,不管在js中仍是在python中,都不是准确的0.3,这是为何呢?算法

缘起chrome

要了解这个问题,首先咱们须要知道浮点数在计算机中究竟是如何进行存储的?不知道你是怎么想的,总之我开始的第一反应就是假设是32位的存储空间,我可能会按照整数的存储方式去想象,好比1-24位是整数位,剩余的8位表明小数,这样能够吗?固然是能够的,可是先考虑下下面的这个问题:数据库

56365a334d4d4f13bf10904f9f3766a6


想象红色区域是所能放置的数字的最大空间,如今有个问题,当咱们想继续加0的时候,发现放不下了,由于空间是有限的,这个时候,咱们会怎么办?ide

8a5695d1febf4983a08efe13c1c84cc1


对,没错,科学计数法,就是咱们在学习过程当中,若是位数太多,咱们通常都会用科学计数法来表示,这样的好处是,书写的位数小,表示的位数多,因此,回到计算机中,32位来表示实数的话,最多能表示多少位?2^32次方个,大约就是40亿,40亿数字不少吗?多,可是和无限多的实数集来比,沧海一粟,不够看的,因此计算机的设计者就要考虑这个问题了,如何让计算放下更多的数字?学习

真的有“定点数”编码

还记得上面说的,1-24表示整数位,剩余的表示小数位吗?这种存储方式就叫定点数,1-24位每4位表示一个0~9的数字的话,能够有6位表示整数部分,剩余2位表示小数部分,这样咱们能够用32位表示从0到999999.99这样1亿个实数,这种用2进制来表示10进制的方式,叫作BCD编码(Binary-Coded Decimal),好比说8421码,从左往右的权依次是8,4,2,1,等等,有兴趣的能够去了解一下。spa

“定点数”存在哪些问题设计

定点数有几个明显的缺点:3d

  • 占了很大的位数,可是能表示的数字范围倒是有限的;

  • 没法同时表示很大的数字和很小的数字

其实究其根本缘由,仍是这种方式的“有限”限制了它,那么有没有一种方式,可让32位所能表示的数字,更“无限”一点,更适合咱们的诉求?

固然,设计计算机的前辈智慧是无限的~

浮点数是如何表示的

就像使用科学计数法同样,计算机前辈在浮点数的设计中也用了同样的思想,IEEE的标准定义了2个基本的浮点数格式,一个是32位的单精度浮点数,一个是64位的双精度浮点数,也就是float或float32和double或float64这两个数据格式,双精度和单精度的表示形式是差很少的,咱们以单精度的做为了解和学习。

da8c56a474ae479bb941f324440b5d9b


分为3部分:

  1. 第一部分是符号位,用s表示,表明正负,要记住的是在浮点数的范围内,全部数字都是有符号的;

  2. 第二部分是指数位,用e表示,表明指数,用8位bit表示的数字范围是0~255,为了同时表示大数和小数,咱们把0~255去掉头尾(0,255后面会用到)的1~254去映射到-126~127,这样同时能够表示最大最小数字;

  3. 第三部分是有效数位,用f表示,表明的是有效的数位;

综合上述表示和科学计数法,咱们的浮点数就能够表示为公式

(-1)^s * 1.f * 2^e

看完公式有没有发现问题?你会发现,咱们这个公式没法表示0,的确,这是一个巧妙的设计,咱们用0(8个bit都为0)和255(8个bit都为1)来表示一些特殊的数值,能够认为他们2个是特殊的flag位,好比当e和f都为0的时候,咱们就认为这个浮点数是0,看下表:

84ee01d094654783a657410129e7f02b


以0.5为例,0.5的符号位s是0,f也是0,e是-1,

这样(-1)^0 * 1.0 * 2 ^ -1 = 0.5

用32位bit表示就是

s e f 0 0111 1110 0000 ...0 1位 8位 23位 0.5 经过这样的表示方式,能够明显的发现32位所能表示的实数范围是很大的,又由于这种方式建立的实数中小数点的位置是能够”浮动“的,因此也被叫作浮点数,

到这里咱们知道了浮点数是怎么存储的了,可是还没解决咱们开始的问题,为什么0.1+0.2!=0.3,首先咱们要知道0.1是怎么存储的:

(-1)^s * 1.f * 2^e = 0.1

求解e

s=0 f=0 e=Math.log2(0.1) // -3.321928094887362

能够看出来这里0.1是算不出来一个准确数字的,从0.1到0.9只有0.5是能够求出一个准确的值的,剩下的都算不出来一个准确的值,这也就是为何0.1+0.2会致使的精度问题,也就是说浮点数不管是表示仍是计算其实都是近似计算,而近似计算就必定会致使一些问题,好比,你但愿银行给你存钱以及算利息的时候用浮点数计算吗?固然不但愿,不然你的钱算多了还好,算少了岂不是亏大了~

浮点数&二进制

把一个二进制表示的浮点数(0.1001),转为10进制表示,由于小数点后的每一位都表示的是2的-N次方,所以转为10进制就是:

(1 * 2 ^ -1) + (0 * 2 ^ -2) + (0 * 2 ^ -3) + (1 * 2 ^ -4) = 0.5625

能够理解为,对于二进制转十进制来讲,从小数点开始,往左就是把2的指数从0开始过一位+1,包括0,往右就是从-1开始依次-1。

把一个10进制的浮点数,转为二进制的话,和整数的二进制表示采用“除以 2,而后看余数”的方式相比,小数部分转换是用一个类似的反方向操做,就是乘以2,而后看是否大于1,若是大于1就记下1并把结果减去1,一直重复操做。

好比,十进制的9.1,小数部分0.1转为2进制的过程为:

1a484ce1c05b4218badbfc1d1d8897ae


这是获得一个无限循环的部分”0011“,整数部分9转为二进制就是1001,所以结果就是1001.000110011...

把小数点作移3位,获得一个浮点数的结果是 1.001000110011... * 2 ^ 3

找到咱们上面的公式 (-1)^s * 1.f * 2^e 套公式可获得:

s = 0 f = 00100011001100110011 001(到23位后自动舍弃,由于最长只能放23位有效数字)

指数位是3,咱们e的范围是1-254 对半分正数和负数,因此127表示0,从127开始加3,获得结果是130,130转为二进制表示结果就是: 1000 0010, 因此获得e=1000 0010, 结果以下:

6c33cac8d87946f7946863682dcda093


因此最终的二进制表示结果是: 0100 0001 0001 0001 1001 1001 1001 1001

若是咱们再把这个浮点数表示换算成十进制, 实际获得的准确值是 9.09999942779541015625。相信你如今应该不会感受奇怪了。

当心你的“存款”

首先,咱们了解一下浮点数的加法计算过程是怎么样的,拿0.5 + 0.125来作计算,首先0.5套用公式计算结果是:

s = 0 有效位1.f = 1.0000... e = -1;

0.125 转换为:

s = 0 有效位1.f = 1.0000... e = -3;

而后,计算口诀是 指数位先对齐(小转大,这里要把e统一为-1), 而后按位相加符号位和有效位,e保持统一后的结果,所以:

符号位s 指数位e 有效位1.f 0.5 0 -1 1.0 0.125 0 -3 1.0 0.125对齐指数位 0 -1 0.01 0.5 + 0.125 0 -1 1.01 结果就是 (-1)^0 * 1.25 * 2^-1 = 0.625;

ps: 为啥是1.25?虽然咱们计算得出的是1.01 可是不要忘记计算是经过2进制算的,计算十进制的时候要转回来哦,因此0100000.... 后面都是0不用管,小数部分,从头开始乘以2的-N次别忘了,因此结果就是2^-2 = 0.25 加上整数位的1 就是1.25了~

能够发现,其实浮点数的计算过程,经过一个加法器也是能够实现的,电路成本一样不会很高,可是须要注意一些别的问题:

计算过程当中,须要先对齐,可是有效数位的长度是23位,假若有一个很大的数字和一个很小的数字进行相加,而后对齐的过程当中,小数被0部位过程当中直接溢出了,23位不够用了,就会出现问题,补完后一些有效位被丢掉了,从而致使结果上的偏差,两个数的指数位差超过23,好比到2^24位(差很少1600万倍),这2个数相加后,结果就直接是较大数,较小数彻底被抛弃了。。。

有些同窗会急急忙忙去chrome的控制输入下面的代码:

Math.pow(2, 24) + 0.1 // 16777216.1 复制代码

骗人,结果不是还有0.1吗,别急,小伙伴,js内置的Number是64位的,你能够试试

Math.pow(2, 50) + 0.1 // 1125899906842624复制代码

是否是小数没了?【这种现象也叫大数吃小数】

因此若是银行采用IEEE-754 32位的浮点数计数方法来保管存款的话,假设你是一个大老板,你的帐户中有2000万rmb,这个时候你的某一个员工给你打了1块钱,哈哈对不起,银行给算丢了,你的存款是不变的!因此,通常银行啊,电商一类的都会在涉及到钱的时候使用定点数或者整数来计算,避免出现精度丢失的问题,若是你去银行涉及数据库,必定要当心谨慎~

总结

这篇文章咱们从浮点数的表示开始,到存储,到转换以及计算过程分析了真实的计算机世界中浮点数究竟是怎么运行的,从中也了解了浮点数究竟为什么会丢失精度:

  1. 浮点数在存储的时候可能出现不能准确转化为对应2进制的状况

  2. 在计算过程当中,又存在大数吃小数的可能,也会致使数据不许确

延伸

精度丢失不是无法解决的,有成熟的方案,不作过多介绍,有兴趣你们能够去研究:

Summation Formula 算法

说明:

文章内容大部分参考自 徐文浩 老师的 「深刻浅出计算机组成原理」专栏,加了一些本身的理解作了一个简单的总结,以后还会继续不定时的分享一些本身的所得,若是以为还不错,点个赞吧~

ps: 有同窗可能会问,既然只有0.5能够转为一个准确的数字,为什么0.1+0.1没有问题,这个我还没仔细研究,不过我猜测是由于自己计算就是一个计算近似值的过程,所以再得出结果后,若是还在一个近似范围内,就会认为没有偏差,超过这个范围,则会认为出现偏差了,总之咱们能够确认的是计算过程当中拿到的确实是一个近似数了,这个也确实是致使一些浮点数计算丢失精度的缘由~

有兴趣的话能够到这里查看实际的数字在计算机中存储的具体内容~

7a7095af9afe42929f706a7e8026e5d4

相关文章
相关标签/搜索