30

【3分钟技能get】js浮点数计算精度问题

 5 years ago
source link: http://mp.weixin.qq.com/s?__biz=MzI1NDYzMjA4NQ%3D%3D&%3Bmid=2247483849&%3Bidx=1&%3Bsn=bbadcd8506dfcdc8aaaa8d8c50998815
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

先看如下计算的输出:

0.1 + 0.2

显然是0.3。但是在javascript中,结果是什么呢?

0.30000000000000004

这是程序语言在数值计算中很容易出现的精度问题,如下图饿了么账单页金额显示。

63AF7bu.png!web

问题产生的原因

先来看对Number类型数值二进制的表示,由3部分组成:

符号位 * 指数位 * 尾数位

由于js采用64位双精度浮点数编码,实际存储时为了节省空间,采用科学计数法表示,其二进制构成如下:

BnAvueq.png!web

符号位占1位,指数位占11位,尾数占52位。

问题分解

0.1的二进制表示为:

0.0001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 ...

其科学计数法表示为:

1.1001... * 2^-4

其中指数位采用偏置码处理,-4即为:01111111011。简单介绍下:

双精度采用的偏置码为1023,
比如指数位:01111111011,其值为1019,
1019 - 1023 = -4

由于尾数位仅为52位,因此需要截取前52位,并且如若第53位为1则进1,反之舍去,因此0.1的尾数位截取后为:

//10011001 10011001 1001100 110011001 10011001 10011001 10011001...
//由于53位为1,进1,即为:
10011001 10011001 10011001 10011001 10011001 10011001 1010

可以看到0.1的值其实已经不准确了,由于进1操作,导致较原值偏大。其对应的二进制存储表示如下:

7RjeY3m.jpg!web

有的童鞋可能注意到了,尾数位存储的是小数部分,这是因为规格化后的值通式为1.x,因此可以略去1,节省了一个bit位空间。

同理0.2的二进制如下:

0.001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001...

科学计数法处理后的二进制存储为:

IjIFv2N.png!web

至此,已经清楚了javascript对数值的存储方式。

进制转换网址参见:http://www.binaryconvert.com/。

再看0.3的二进制表示:

0.010011001100110011001100110011001100110011001100110011...

使用科学计数法表示后存储为:

Bfu6f2a.png!web

而计算机在处理0.1+0.2时(上面已经知道了其分别对应的二进制存储方式),需要通过对阶、尾数求和、规格化、舍入等操作(这里不再赘述),最终得到:

0.010011001100110011001100110011001100110011001100110100
//转为10进制即为:0.30000000000000004

可以知道,计算机在进行浮点数加减运算时,包括对阶、规格化过程都可能产生精度误差,核心还是因为尾数位的位数有限,0舍 1进 导入的误差。

如何在开发中避免此类问题?

1. 取固定精度

有的童鞋可能会采用toFixed()获取固定精度,如下

(0.1+0.2).toFixed(1) = 0.3;

对于精度要求不高的话,这种通过4舍5入获取固定精度的方式一般可以满足需求。

2. 先将小数转为整数再进行计算

0.1 + 0.2
//获取两者都能转化为整数的最小公倍数:RATE = 10

//将上式转换为:

(0.1*RATE + 0.2*RATE)/RATE

= 0.3

这是日常开发中最常用的方式,推荐。

扩展

如果清楚上面讲解的数值存储方式,那么可以知道js的安全整数范围为:

Math.pow(2, 53) - 1  
// 可表示的安全整数范围:
// Number.MIN_SAFE_INTEGER ~ Number.MAX_SAFE_INTEGER
-9007199254740991 ~ 9007199254740991

超出这个范围的整数计算会出现精度丢失问题。

需要处理较大值的话,可以参考bignumber.js等;另外ES2020,加入了BigInt类型:

let number1 = BigInt(123);  //方式1
let number2 = 123n;  //方式2
number1 == number2;  //true
typeof number1; //"bigint"

谷歌浏览器已经支持了,可以尝试下~

获取更多干货分享,欢迎【点赞关注】~

Ubiq2i3.png!web

随手点个 在看 鼓励下吧~ mERFRbB.jpg!web


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK