• 为什么0.1+0.2 !== 0.3,而 0.1+0.3 === 0.4


    最近看了一本书《代码之髓》,里面提到浮点数在计算机的存储方式——IEEE 754 会引起浮点数的精度丢失问题。这让我想起了“著名”的 JS 问题:为什么 0.1 + 0.2 !== 0.3 ?

    迷迷糊糊的就记得是浮点数精度丢失原因造成的,但问到具体是怎么回事儿就傻眼了。

    今天就尝试用基本知识来推理下。

    十进制浮点数转二进制

    众所周知,所有数据都是以二进制形式保存在计算机中的。浮点数如何转化为二进制数呢?

    • 整数部分:除以2,取出余数,商继续除以2,直到得到0为止,将取出的余数逆序。
    • 小数部分:乘以2,然后取出整数部分,将剩下的小数部分继续乘以2,然后再取整数部分,一直取到小数部分为零为止。如果永远不为零,则按要求保留足够位数的小数,最后一位做0舍1入。将取出的整数顺序排列。

    譬如对于 8.75, 转二进制计算过程如下:

    8/2:4 余 0,
    4/2:2 余 0,
    2/2:1 余 0,
    1/2:0 余 1
    所以 8 的二进制为 1000
    
    0.75*2 = 1.5,取整 1,小数部分为 0.5,
    0.5*2 = 1.0,取整 1,小数部分为 0
    所以 0.75 的二进制是 0.11
    

    最终得到 8.75 等于二进制数 1000.11。

    0.1,0.2,0.3,0.4 的二进制转化

    通过上面的计算方式,可以得出 0.1,0.2,0.3,0.4 对应的二进制数:

    // 括号内表示数字无限循环
    0.1 -> 0.000110011(0011)
    0.2 -> 0.00110011(0011)
    0.3 -> 0.010011(0011)
    0.4 -> 0.0110011(0011)
    

    JS 数字存储方式

    在实际存储中,不可能保存无限长度的数据,JS 采用 IEEE 754 双精度64位浮点数来保存数字,格式为s * m * (2^(e)),其中 s 表示符号位,m 表示尾数占52位,e 表示指数占11位。

    我们来看看上面几个数在计算机内的表示,也可以在这个网站验证结果:

    // 0.1
    e = -4;
    m = 1.1001100110011001100110011001100110011001100110011010 (52位)
    
    // 0.2
    e = -3;
    m = 1.1001100110011001100110011001100110011001100110011010 (52位)
    
    // 0.3
    e = -2
    m = 1.0011001100110011001100110011001100110011001100110011 (52位)
    
    // 0.4
    e = -2
    m = 1.1001100110011001100110011001100110011001100110011010 (52位)
    

    特别注意的是,对于无限长度的数据,在存储过程中,会有数据的舍入【二进制向最近偶数舍入】,这造成了后面数据计算的误差。

    0.1 + 0.2 !== 0.3

    现在我们来做二进制的加法计算:
    1.1001100110011001100110011001100110011001100110011010 (Exponent:-4)+ // 0.1
    1.1001100110011001100110011001100110011001100110011010 (Exponent:-3)= // 0.2

    这里有一个问题,就是指数不一致时,应该怎么处理,一般是往右移,因为即使右边溢出了,损失的精度远远小于左移时的溢出。

    也就是指数往更大值看起,尾数不够添就 0。

    0.11001100110011001100110011001100110011001100110011010 (Exponent:-3)+ // 指数由 -4 变为 -3,小数部分右移 1 位
    1.10011001100110011001100110011001100110011001100110100 (Exponent:-3)= // 尾数长度不够,加一个0
    10.01100110011001100110011001100110011001100110011001110 (Exponent:-3)

    计算结果小数部分 53 位,整数部分为 10,转换为IEEE754双精度为 1.0011001100110011001100110011001100110011001100110100 * 2^(-2)。

    如果用二进制转成十进制为 2^(-2) + 2^(-5) + 2^(-6)...。 结果大约是0.30000000000000004419,去小数点后面17位精度为0.30000000000000004。

    0.1 + 0.3 === 0.4

    1.1001100110011001100110011001100110011001100110011010 (Exponent:-4)+ // 0.1
    1.0011001100110011001100110011001100110011001100110011 (Exponent:-2) // 0.3

    这里的处理方式同上:

    0.011001100110011001100110011001100110011001100110011010 (Exponent:-2)+ // 指数由- 4变为 -2,小数部分右移 2 位
    1.001100110011001100110011001100110011001100110011001100 (Exponent:-2)= // 尾数不够,加两个 0
    1.100110011001100110011001100110011001100110011001100110 (Exponent:-2)

    计算结果有 54 位,保存时需要二进制舍入,得到:
    1.1001100110011001100110011001100110011001100110011010 (Exponent:-2) // 52 位

    这个结果,恰好等于 0.4。

    结论

    JS 浮点数转化为二进制数进行存储,由于存储的长度有限制,就会有数据的舍入而导致精度丢失。

    浮点数的计算,会转化为二进制进行计算,计算结果又可能丢失精度。即使结果看起来正确,也只是碰巧而已。

  • 相关阅读:
    Struts2 Hello World
    Struts2入门(1)
    Struts2_day01
    Java Web Model2实战
    Oracle_day04
    SAP调用外部webservice接口
    通用清账程序
    服务器IDOC文件解析程序
    IDOC接口创建步骤
    SAP 本地发送IDOC
  • 原文地址:https://www.cnblogs.com/fayin/p/13926186.html
Copyright © 2020-2023  润新知