最近在项目中遇到问题,console.log(5.1 - 3)
,结果为 2.0999999999999996
,查阅相关资料了解到,原来是浮点数计算的问题。关于浮点数最突出的例子就是 0.1 + 0.2 = 0.30000000000000004
。我们都知道是因为在转二进制的时候,0.1和0.2不能用二进制精确表示,会丧失精确,但是小数位的二进制怎么计算?精确值又是怎么丢失的呢?
在JavaScript中,number类型(不区分整数值和浮点数值)都是以 64bit 双精度浮点数存储的,双精度的表现形式如下图:
S(符号位) | E(指数位) | F(尾数位) | |
---|---|---|---|
位数 | 1 | 11 | 52 |
位置 | 63 | 62 - 52 | 51 - 0 |
浮点数在计算机中的表示是基于科学计数法的,比如 1234
,用科学计数法表示应该是 1.234 * 10³
,这里分为3部分,对照上表,第一部分是符号位,1表示负数,0表示正数,第二部分是指数位,用接下来的11位表示,这里的指数有偏移值。因为指数也是可正可负,处理方法有2种,第一种的在指数位也添加一个符号位,第二种方法就是设置一个 偏移 ,使指数部分一直表现为一个非负数,然后减去偏移量才是真正的指数,而64bit设置的偏移量是 1023 。第三部分是尾数位(有效数字),当尾数的值不为0时,尾数的最高有效位应为1,这称为浮点数的规格化,可以节省出一位来用于提高精度,这里的尾数位就是1.234。接下来我们看一下精度是怎么丢失的。
十进制整数转换为二进制整数采用 除2取余,逆序排列 法,这个我们都很清楚了,所以我们主要说一下十进制小数怎么转换为二进制。 小数位转二进制采用 乘2取整,顺序排列 的方法,比如十进制的4.125,我们分为以下几步:
4.125
整数部分4转换为二进制是 100
,小数位 0.125
根据 乘2取整,顺序排列 计算
0.125 * 2 = 0.25 -------- 取整数 0
0.25 * 2 = 0.5 -------- 取整数 0
0.5 * 2 = 1.0 -------- 取整数 1
所以 4.125
转为二进制就是 100.001
。
从上面我们知道,浮点数的规格化是尾数位最高位为1,那么我们转换一下得到 1.00001 * 2^2
- 符号位:正数,所以为0
- 指数位:指数位是2,加上偏移量1023,E = 1025
- 尾数位:1.00001 取小数部分为 00001
所以 4.125
转为浮点数就是
S(符号位) | E(指数位) | F(尾数位 |
---|---|---|
0 | 10000000001 | 0000100....0000 |
1位 | 11位 | 52位 |
我们用 0.1 + 0.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
0.4 * 2 = 0.8 ------ 取整数 0
0.8 * 2 = 1.6 ------ 取整数 1
0.6 * 2 = 1.2 ------ 取整数 1
0.2 * 2 = 0.4 ------ 取整数 0
所以0.1转换为二进制就是 0.0001100110011.....00110011
,我们发现无限循环了,但是浮点数的尾数只能放下52位,那剩下的只能舍弃了,所以这里就丢失了精度。还有当0.1和0.2被存储时,存进去的已经不是精确的0.1和0.2了,而是精度发生一定丢失的值,当这个两个值发生相加时,精度还可能进一步丢失。