3

探寻JavaScript精度问题

 1 year ago
source link: http://muyunyun.cn/blog/dzjdfedl/
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

探寻 JavaScript 精度问题

阅读完本文可以了解到 0.1 + 0.2 为什么等于 0.30000000000000004 以及 JavaScript 中最大安全数是如何来的。

十进制小数转为二进制小数方法

拿 173.8125 举例如何将之转化为二进制小数。

①. 针对整数部分 173, 采取除 2 取余, 逆序排列;





173 / 2 = 86 ... 1
86 / 2 = 43 ... 0
43 / 2 = 21 ... 1 ↑
21 / 2 = 10 ... 1 | 逆序排列
10 / 2 = 5 ... 0 |
5 / 2 = 2 ... 1 |
2 / 2 = 1 ... 0
1 / 2 = 0 ... 1

得整数部分的二进制为 10101101

②. 针对小数部分 0.8125, 采用乘 2 取整, 顺序排列;





0.8125 * 2 = 1.625 |
0.625 * 2 = 1.25 | 顺序排列
0.25 * 2 = 0.5 |
0.5 * 2 = 1 ↓

得小数部分的二进制为 1101

③. 将前面两部的结果相加, 结果为 10101101.1101;

小心, 二进制小数丢失了精度!

根据上面的知识, 将十进制小数 0.1 转为二进制:





0.1 * 2 = 0.2
0.2 * 2 = 0.4 // 注意这里
0.4 * 2 = 0.8
0.8 * 2 = 1.6
0.6 * 2 = 1.2
0.2 * 2 = 0.4 // 注意这里, 循环开始
0.4 * 2 = 0.8
0.8 * 2 = 1.6
0.6 * 2 = 1.2

可以发现有限十进制小数 0.1 却转化成了无限二进制小数 0.00011001100..., 可以看到精度在转化过程中丢失了!

能被转化为有限二进制小数的十进制小数的最后一位必然以 5 结尾(因为只有 0.5 * 2 才能变为整数)。所以十进制中一位小数 0.1 ~ 0.9 当中除了 0.5 之外的值在转化成二进制的过程中都丢失了精度。

推导 0.1 + 0.2 为何等于 0.30000000000000004

在 JavaScript 中所有数值都以 IEEE-754 标准的 64 bit 双精度浮点数进行存储的。先来了解下 IEEE-754 标准下的双精度浮点数

dda7d5b38676abfa13afb344f8a792ed.jpg-300

这幅图很关键, 可以从图中看到 IEEE-754 标准下双精度浮点数由三部分组成, 分别如下:

  • sign(符号): 占 1 bit, 0 表示正, 1 表示负;
  • exponent(指数): 占 11 bit, 表示范围;
  • mantissa(尾数): 占 52 bit, 表示精度, 多出的末尾如果是 1 需要进位;

推荐阅读 JavaScript 浮点数陷阱及解法, 阅读完该文后可以了解到以下公式的由来。

5c268e4bd6e0bf2466598d9d5cb58a16.jpg-200

精度位总共是 53 bit, 因为用科学计数法表示, 所以首位固定的 1 就没有占用空间。即公式中 (M + 1) 里的 1。另外公式里的 1023 是 2^11 的一半。小于 1023 的用来表示小数, 大于 1023 的用来表示整数。

指数可以控制到 2^1024 - 1, 而精度最大只达到 2^53 - 1, 两者相比可以得出 JavaScript 实际可以精确表示的数字其实很少。

0.1 转化为二进制为 0.0001100110011..., 用科学计数法表示为 1.100110011... x 2^(-4), 根据上述公式, S0(1 bit), E-4 + 1023, 对应的二进制为 01111111011(11 bit), M1001100110011001100110011001100110011001100110011010(52 bit, 另外注意末尾的进位), 0.1 的存储示意图如下:

5b7c0dcc0b1770b6eed238e288eb4c0e.jpg-300

同理, 0.2 转化为二进制为 0.001100110011..., 用科学计数法表示为 1.100110011... x 2^(-3), 根据上述公式, E-3 + 1023, 对应的二进制为 01111111100, M1001100110011001100110011001100110011001100110011010, 0.2 的存储示意图如下:

cb0ef89aa0ed6e8d32b90d1a29cfc9e1.jpg-300

0.1 + 0.2 即 2^(-4) x 1.1001100110011001100110011001100110011001100110011010 与 2^(-3) x 1.1001100110011001100110011001100110011001100110011010 之和





// 计算过程
0.00011001100110011001100110011001100110011001100110011010
0.0011001100110011001100110011001100110011001100110011010
// 相加得
0.01001100110011001100110011001100110011001100110011001110

0.01001100110011001100110011001100110011001100110011001110 转化为十进制就是 0.30000000000000004。验证完成!

JavaScript 的最大安全数是如何来的

根据双精度浮点数的构成, 精度位数是 53 bit。安全数的意思是在 -2^53 ~ 2^53 内的整数(不包括边界)与唯一的双精度浮点数互相对应。举个例子比较好理解:





Math.pow(2, 53) === Math.pow(2, 53) + 1 // true

Math.pow(2, 53) 竟然与 Math.pow(2, 53) + 1 相等!这是因为 Math.pow(2, 53) + 1 已经超过了尾数的精度限制(53 bit), 在这个例子中 Math.pow(2, 53)Math.pow(2, 53) + 1 对应了同一个双精度浮点数。所以 Math.pow(2, 53) 就不是安全数了。

最大的安全数为 Math.pow(2, 53) - 1, 即 9007199254740991

业务中碰到的精度问题以及解决方案

了解 JavaScript 精度问题对我们业务有什么帮助呢?举个业务场景: 比如有个订单号后端 Java 同学定义的是 long 类型, 但是当这个订单号转换成 JavaScript 的 Number 类型时候精度会丢失了, 那没有以上知识铺垫那就理解不了精度为什么会丢失。

解决方案大致有以下几种:

1.针对大数的整数可以考虑使用 BigInt 类型(目前在 stage 3 stage4 阶段);

2.使用 bigNumber, 它的思想是转化成 string 进行处理, 这种方式对性能有一定影响;

3.可以考虑使用 long.js, 它的思想是将长度为 64 位的 long 类型值转化成两个精度为 32 位的双精度类型的值。

4.针对小数的话可以使用 number-precision, 该库将小数转为整数后再作处理;


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK