​阅读本文大约需要8分钟...

问题

在计算机的世界里,可能有很多常人无法理解的事情。比如 0.1 + 0.2 = ?。来,告诉我你的答案。

有的朋友看到这就迫不及待的说,这么简单的问题,很明显等于 0.3 啊,小学生都会算的好伐。你这是在侮辱我的智商?

好吧,我来告诉你一个打脸的事实,0.1 + 0.2 还真不等于 0.3 。先别急着反驳我。

打开你的任意一个浏览器(我用chrome做演示),F12打开console控制台,输入 console.log(0.1 + 0.2)
。如果你操作正确的话,你会看到以下的结果。



是不是感觉匪夷所思,what?为什么结果是0.30000000000000004,这是神魔鬼? 难道,我这么多年学习的数学知识,老师教的都是错的?

别着急。其实,你的老师教的没错,在我们的世界中,0.1 + 0.2 确实是等于 0.3 的。但是,在计算机中,可就不是这么一回事了。待我娓娓道来。


因为,我们在计算数学问题的时候,用的是十进制,计算出来结果是0.3没问题。但是,在计算机中用的是二进制,都是由0和1来组成。这就不得不提一下,十进制转换二进制了。

二进制转换

十进制小数转换二进制的步骤:(以10.25为例)

1.先转换整数部分,除2直到商为0,倒数取余。
10/2 ... 商5...余数0 5/2 ...商2...余数1 2/2 ...商1...余数0 1/2 ...商0...余数1
倒数取余,就是1010

2.再转换小数部分,乘2取整,直到小数部分为0.
0.25*2 ... 0.50 ...整数0 0.50*2 ... 1.0 ...整数1
小数部分为0,结束,即为01

因此10.25(10)转换成二进制,结果就是 1010.01(2)

聪明的你,类比以上方法,应该可以动手去算一下十进制0.1转成二进制是多少了。
0.1*2 ... 0.2 ...整数0 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
等等,怎么感觉进入死循环了,小数部分乘以2,一直乘不到小数部分为0

就像十进制中1/3,结果是0.3(3...)这样的问题一样,0.1转成二进制时也会存在精度问题,我们需要进行取舍。

我们看一下0.1在计算机中是怎么存储的。对此,需要了解一下浮点数的概念。

浮点数


浮点数,顾名思义,小数点是浮动的数。千万不要以为浮点数就是小数。因为,在js中是没有整数和小数的概念的,其实整数也是以浮点数的形式表示的,只是小数部分为0而已。

浮点数简单理解,就是类似于我们十进制中的科学计数法。在计算机中一般遵循IEEE 754标准。格式如下:
(-1)^S * M * 2^E 1. S表示符号位,当S=0时,为正数;当S=1时,为负数。 2. M表示有效数字(尾数),大于等于1,小于2。 3.
E为指数(也叫阶码)。
因此,上边的10.25(二进制1010.01)按照此格式表示即为 1.01001 * 2^3

对于32位浮点数来说,符号位占一位,指数位占8位,尾数占23位
对于64位浮点数来说,符号位占一位,指数位占11位,尾数占52位

IEEE 754标准

注意:IEEE
754标准规定,在保存尾数M时,第一位默认是1,因此可以被舍去,只存储后边的部分。例如,1.01001保存的时候,只保存01001,等到用的时候再把1加上去。这样,就可以节省一个位的有效数字。

指数E在存储的时候也有些特殊。若为32位,指数占8位,则可表示的大小范围为0-255 。如为64位,指数占11位,范围为0-2047
。但是,指数是有正有负的,因此实际值需要在此基础上减去一个中间数。对于32位,中间数为127,对于64位,中间数为1023 。

还是以1.01001 * 2^3 为例,若为32位浮点数,则需要保存成 3+ 127 = 130,即二进制的10000010,若为64位浮点数,则保存成
3+ 1023 = 1026 ,即二进制的10000000010。

计算步骤

好了。巴拉巴拉了这么多。终于,要进入我们今天的正题了。

我们看一下 0.1 在计算机中是怎么用 IEEE 754标准存储的。

十进制0.1转为二进制为0.0001100110011(0011循环),即
1.100110011(0011)*2^-4,因此符号位为0,尾数1.100110011(0011),阶码为 -4,实际存储为 -4+1023 = 1019
的二进制 1111111011
0 01111111011 1001100110011001100110011001100110011001100110011010 S E指数 M尾数
十进制0.2转为二进制为0.001100110011(0011循环),即 1.100110011(0011)*2^-3 ,存储时,符号位为0,尾数
1.100110011(0011),阶码为-3,实际存储为 -3+1023 = 1020 的二进制 1111111100。
0 01111111100 1001100110011001100110011001100110011001100110011010 S E指数 M尾数
接下来,计算 0.1 + 0.2 。


浮点数进行计算时,需要对阶。即把两个数的阶码设置为一样的值,然后再计算尾数部分。其实对阶很好理解,就和我们十进制科学记数法加法一个道理,先把指数部分化成一样,再计算尾数。


另外,需要注意一下,对阶时需要小阶对大阶。因为,这样相当于,小阶指数乘以倍数,尾数部分相对应的除以倍数,在二进制中即右移倍数位。这样,不会影响到尾数的高位,只会移出低位,损失相对较少的精度。

因此,0.1的阶码为 -4 , 需要对阶为 0.2的阶码 -3 。尾数部分整体右移一位。
原来的0.1 0 01111111011 1001100110011001100110011001100110011001100110011010
对阶后的0.1 0 01111111100 1100110011001100110011001100110011001100110011001101
然后进行尾数部分相加
0 01111111100 1100110011001100110011001100110011001100110011001101 + 0
01111111100 1001100110011001100110011001100110011001100110011010 = 0
01111111100 10110011001100110011001100110011001100110011001100111
可以看到,产生了进位。因此,阶码需要 +1,即为 -2,尾数部分进行低位四舍五入处理。因尾数最低位为1,需要进位。所以存储为:
0 1111111101 1011001100110011001100110011001100110011001100110100
最后把二进制转换为十进制,
二进制为: 1.1011001100110011001100110011001100110011001100110100 * 2^-2 转为十进制为:
2^-2 * (1*2^0 + 1*2^-1 + 0 + 1*2^-3 + 1*2^-4 + ...) 最终结果为:
0.3000000000000000444089209850062616169452667236328125 因为精度问题,只取到:
0.30000000000000004
问题总结

1.在十进制转换为二进制的过程中,会产生精度的损失。

2.二进制浮点数进行对阶运算时,也会产生精度的损失。

因此,最终结果才产生了偏差。

看完的小伙伴,现在应该能理解,为什么0.1 + 0.2 ≠ 0.3 这个问题了吧。