老生常谈:0.1+0.2=0.30000000000000004 的运算过程

336 阅读6分钟

为啥JS中 0.1+0.2=0.30000000000000004?

前言

  • 关于这个问题,其实网上已经有了很多解答。
    • 大体意思都是=》
      1. 计算机内存采用二进制来存储十进制的浮点数,这样会有精度丢失(由于位数有限)。
      2. 对于浮点数的存储和运算方式,要参考 IEEE754的说明。(乍一看可能很蒙,哈哈哈)。image-20210712233229196.png
      3. JS中对于小数位最多支持17位。(按照浮点数转换后的规则计算0.1+0.2得到的结果应该是,0.3000000000000000444089209850062616169452667236328125,JS只四舍五入截取了前17位)

有趣的点

  • 关于这个问题,我觉得深入研究一下还是挺有意思的。我对于浮点数在内存里到底是怎么存储的,之前只能算是了解一点,纸上谈兵。(以前看过Stanford的cs107公开课,感觉对内存这块讲的很有趣,推荐老铁们食用)
  • 今天,我就准备手撕一下浮点数的运算过程,学以致用。(最多也就64个bit)`
  • 首先有几个概念是必备的:
      1. 10进制到2进制的转换(整数与小数部分)
      2. 对于 2进制数据在位数不够要截取时,遵循的四舍五入规则
      3. 对于 IEEE754给的表述,要充分理解基本意思。
      4. JS 对于数字,只有number这个基本数据类型的描述(包装类暂且不论)。对于小数用8个字节来存储(也就是 IEEE754 所说的64位存储方式)

正题

  • 首先,咱们手动把10进制的0.1转换成2进制 =》

    0.1 (10) = 0.000110011[0011循环] (2)

    0.2 (10) = 0.00110011[0011循环] (2)

    PS:因为0.2是0.1的二倍,所以0.1二进制整体左移一位就能得到0.2

 随便以6.6为例复习一下进制转换
   => 6.6=6+0.6
 ​
 整数部分:
   6 => 6 /2= 3    余0
        3 /2= 1    余1
        1 /2= 0.5  余1
       余数倒序为 110,即 6(10) = 110(2)
       
 小数部分:
   0.6 => 0.6 *2= 1.2
          0.2 *2= 0.4
          0.4 *2= 0.8
          0.8 *2= 1.6
       ==这里开始无限循环,除了0.5,0.25,0.125这样的数外,大部分都是无限循环的
          0.6 *2= 1.2
          0.2 *2= 0.4
          0.4 *2= 0.8
          0.8 *2= 1.6
          结果从上到下取计算结果的整数位
       所以,0.6(10)=0.10011001[1001循环]
 ​
 最终,6.6(10) = 110.10011001[1001循环](2)
  • 上面拿到的二进制形式是咱们自己算的,可不是计算机真正的存储形式哦。想知道0.1和0.2在计算机内真正的存储形式,要先搞明白下面这张图。

  • 由于JS用64位来存储浮点数,也就是对应下表的 double规则(没错就是8个字节的double,java,c++啥的也是如此,默认指64位编译器)

  • s表示符号(占1位)

    • 0正,1负
  • exponent表示指数(占11位)

    • 补充一下,bias其实是个中间数,它的作用很简单,就是为了把指数分为正负2部分,因为,指数的值可能为正也可能为负,如果采用补码表示的话,全体符号位S和Exp自身的符号位将导致不能简单的进行大小比较。正因为如此,指数部分通常采用一个无符号的正数值存储。比如:不考虑符号,11位最多可以表示2^11个数,0 ~ 2047(2^11-1),那么中间数就是1023,就可以把指数区间变为 -1023 ~ 1024
  • fraction表示尾数(占52位)

    • 是不是很像科学计数法?我们上面算出来的二进制就是要化成下面这个形式,然后在算出对应的 S,E,F,然后才能倒推得到我们想要的真正内存形式。
  • image-20210713225756110.png

  • image-20210713232933245.png

  • 理解了 iEEE754的标准后,我们需要把上面算出来的结果进行处理

   0.000110011[0011循环] 
       = 1.100110011[0011循环] * 2^-4 
       = (-1)^0 *(1+0.100110011[0011循环])* 2^(1019-1023)
       所以我们知道了,s=0 Exp=1199 Fra=0.100110011[0011循环]
        
       0.1二进制:
        0 01111111011 1001100110011001100110011001100110011001100110011010 
        S E指数        M尾数(这里因为只有52,所以进行了二进制四舍五入)
        PS:这里我们反推一下,把这个二进制位模式恢复成第一行的形式,
            得到 0.00011001100110011001100110011001100110011001100110011010
            参考下图,如果严格人工计算应该不是0.1,但这是计算机的处理方式。

image.png

  
   0.00110011[0011循环]  = 1.100110011[0011循环] * 2^-3
       同样求出,0.2的二进制存储方式(s=0 Exp=1020 Fra=0.100110011[0011循环])
       
       0.2的二进制:
        0 01111111110 1001100110011001100110011001100110011001100110011010 
        S E指数        M尾数
   
  • 知道了小数存储的位模式后我们就可以计算了,但是由于0.1阶码为-4,0.2阶码为-3,这里需要把0.1进行对阶。
那么对阶之后是什么样呢?
原来的0.1    0 01111111011 1001100110011001100110011001100110011001100110011010 
对阶后的0.1  0 01111111100 1100110011001100110011001100110011001100110011001101

如果上面不好理解的话,我就写成下面这样:

开始:
0.1为 e = -4; m =1.1001100110011001100110011001100110011001100110011010 (52位)
0.2为 e = -3; m =1.1001100110011001100110011001100110011001100110011010 (52位)

对阶后:
相加时指数不一致,需要对齐,一般情况下是向右移,因为最右边的即使溢出了,损失的精度远远小于左边溢出
0.1为 e = -3; m =0.1100110011001100110011001100110011001100110011001101 (52位)
0.2为 e = -3; m =1.1001100110011001100110011001100110011001100110011010 (52位)

完整计算过程如下:
  e = -3; m = 0.1100110011001100110011001100110011001100110011001101 
+ e = -3; m = 1.1001100110011001100110011001100110011001100110011010
---------------------------------------------------------------------------
  e = -3; m = 10.0110011001100110011001100110011001100110011001100111
---------------------------------------------------------------------------
  e = -2; m = 1.0011001100110011001100110011001100110011001100110100(52位)
  这一步骤的具体位模式操作是,阶码加一,由于尾数溢出,所以尾数部分,去除最高位1,最后一位1,
  进行舍入,得到52位新的二进制。
---------------------------------------------------------------------------
= 0.010011001100110011001100110011001100110011001100110100
  0.3的存储位模式:0 01111111101 0011001100110011001100110011001100110011001100110100
  手动将其转换成十进制数为:0.3000000000000000444089209850062616169452667236328125

= 0.30000000000000004(十进制,由于精度问题小数点后只保留17位)

image.png