探寻 JavaScript 精度问题

作者: 牧云云 | 来源:发表于2018-10-03 15:52 被阅读42次

    阅读完本文可以了解到 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 标准下的双精度浮点数

    image

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

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

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

    image

    精度位总共是 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 的存储示意图如下:

    image

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

    image

    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 精度问题

        本文链接:https://www.haomeiwen.com/subject/rvnzoftx.html