• 4

  • 545

0.1 + 0.2 为什么不等于 0.3--奇怪的相等与不等

黑猫

我不是黑客

1个月前

现象

0.1 + 0.2 === 0.3
false
复制代码

前置知识

  • 在强类型语言中,整数和小数是区分对待的,整数用整型,小数用浮点型
  • 目前有两种显示浮点数的方法:单精度和双精度,在进行浮点运算时,单精度使用 32 位,而双精度使用 64 位。
  • 本文对浮点型简略带过,关于浮点型不了解的同学,推荐<你不知道的浮点型>

探究

0.1 + 0.2 === 0.3为false为例;

要解释这个问题,必须先解释js中小数存储的原理;事物出现必有其原因,问题驱动:

  1. js中数字类型无区分(整数、小数),那它们转化为二进制后都是怎么存储的呢?

小数有要求(乘R取整,可能出现循环无穷,可见下面的0.1),整数易转化无要求(除R取余),很简单的,要保证小数的运算标准,所以必然是浮点型存储;

  1. js选择的是哪种?

与许多其他编程语言不同,JavaScript 并未定义不同类型的数字数据类型,而是始终遵循国际 IEEE 754 标准,将数字存储为双精度浮点数。

IEEE754 标准的双精度浮点数
img

IEEE 754 浮点数由三个域组成,分别为 sign bit (符号位)、exponent bias (指数偏移值) 和 fraction (尾数)。64 位中,sign bit 占 1 位,exponent bias 占 11 位,fraction 占 52 位。

实操

好的,让我们开始计算模拟计算机开始计算0.1+0.2吧,关键是小数转IEEE 754标准,之后的加和很简单

转化过程相当于把大象装冰箱:

  1. 将 0.1 转换为二进制表示
  2. 将转换后的二进制通过科学计数法表示
  3. 将通过科学计数法表示的二进制转换为 IEEE 754 标准表示
将 0.1 转换为二进制表示

我们都知道小数转二进制用乘R取整的方法,运算如下(进制转换及原补反移码不了解的同学,推荐<浅显易懂的原补反移>)

得到结果0.00011001100110011…(循环0011)

将转换后的二进制通过科学计数法表示

0.00011...(无限重复 0011) 通过科学计数法表示则是 1.10011001...(重复 1001)*2-4

将通过科学计数法表示的二进制转换为 IEEE 754 标准表示

进行规格化,简言之就是求得 exponent bias 和 fraction ,

  • exponent bias (指数偏移值)

    双精度浮点数固定偏移值 (2^(11-1)-1) 加上指数实际值(即 2^-4 中的 -4) 的 11 位二进制表示。为什么是 11 位?因为 exponent bias 在 64 位中占 11 位

    因此 0.1 的 exponent bias 等于 1023 + (-4) = 1019 的11 位二进制表示,即 011 1111 1011。
    复制代码
  • fraction(尾数)

    fraction 占 52位所以抽取 52 位小数(多出来的采用四舍五入制)

    1001...(中间有 11 个 1001)...1010 (请注意最后四位,是 1010 而不是 1001,因为四舍五入有进位,这个进位就是造成 0.1 + 0.2 不等于 0.3 的原因)。
    复制代码

到此,终于可以将 0.1 转换为 IEEE 754 表示了

    0       011 1111 1011   1001...( 11 x 1001)...1010
(sign bit) (exponent bias)      (fraction)
复制代码

警报警报,误差出现

此时如果将这个数转换为十进制,可以发现值已经变为 0.100000000000000005551115123126 而不是 0.1 了。

同理将0.2和0.3同过程,亦会有误差。

加和自然不等。

奇怪的相等

在 javascript 语言中,Number 下分别有两个常量 MAX_VALUEMAX_SAFE_INTEGER

其中,MAX_VALUE 表示在 JavaScript 里所能表示的最大数值,MAX_SAFE_INTEGER 表示在 JavaScript 中最大的安全整数,他们的值分别如下:

Number.MAX_VALUE // 1.7976931348623157e+308
Number.MAX_SAFE_INTEGER // 9007199254740991
复制代码
const a = Number.MAX_SAFE_INTEGER
a + 1 === a + 2 // true
复制代码

前置知识

Number.MAX_SAFE_INTEGER与Number.MAX_VALUE

上面的定义是不是很模糊,最大还能理解,最大安全是指什么?了解思考一番后,我们这样来理解,我们从上面已经得知了js(其实只要遵循 IEEE 754规范的都是)存储数字的规则,问题驱动:

js中最大的数怎么表示?

聪明的你肯定会想到是,从科学计数法的角度而言,自然是尾数最大,指数最大;

  • 尾数即52位全取一就好了。
  • 阶码即11位全取一

很简单对吗?对不起给大家挖坑了,其实不对,这个数在js中属于NAN,为什么呢?龟腚(IEEE754对三种特殊的情况进行了规定)

img

至于为什么有这个规定。。想了很久没想明白,战略性放弃,回头补上。

这样的话,我们就可以得到最大数了

  • 尾数即52位全取一就好了
  • 阶码即11位除最后一位外全取一

得到的是什么呢?(2^53 – 1) 2^(2046 – 1023) = (2^53 – 1) 2^971

这既是Number.MAX_VALUE的值

控制台打印
(Math.pow(2, 53) - 1) * Math.pow(2, 971) // 1.7976931348623157e+308
(Math.pow(2, 53) - 1) * Math.pow(2, 971) === Number.MAX_VALUE // true
复制代码
js中安全范围内最大的整数怎么表示?

所谓的安全,就是大于这个数的整数不一定可以精确表示,其实很好理解,只有尾数溢出了52位,才会出现“四舍五入”的情况,再加上默认存在的首位1,这也就意味着,整数只要在 2^53 – 1内,都是绝对安全的,不会出现精度损失问题;

那么,为什么大于这个数,就会出现呢?我们以此数+1为例,也就是这个相等的奇怪情况

const a = Number.MAX_SAFE_INTEGER
a + 1 === a + 2 // true
复制代码

Number.MAX_SAFE_INTEGER 用原码表示是

0 10000110100 1111111111111111111111111111111111111111111111111111
复制代码

IEEE 754 规格化后

11111111111111111111111111111111111111111111111111111
复制代码

Number.MAX_SAFE_INTEGER+1 的原码是

100000000000000000000000000000000000000000000000000000
复制代码

IEEE 754 规格化后

0 10000110101 0000000000000000000000000000000000000000000000000000 0(溢出,小于0.5r,r代表进制,舍去)
复制代码

Number.MAX_SAFE_INTEGER+1 的原码是

100000000000000000000000000000000000000000000000000001
复制代码

IEEE 754 规格化后

0 10000110101 0000000000000000000000000000000000000000000000000000 1(溢出,为0.5r,r代表进制,且最后一位为0,不进位舍去)
复制代码

注意到我们省去掉了一位,按照向偶舍入的规则,还是不会产生进位。这个时候就有问题了,这个数跟刚才那个数竟然是相等的,我们来验证下

控制台打印
const a = Number.MAX_SAFE_INTEGER
a + 1 === a + 2 // true
复制代码

到此,我们就真正原理上解释了js中奇怪的相等与不等现象

参考文章

JS-为什么 0.1 + 0.2 不等于 0.3 ?

浮点数阶码的计算和表示

JS 中的 MAX_VALUE 和 MAX_SAFE_INTEGER 是怎么来的

免责声明:文章版权归原作者所有,其内容与观点不代表Unitimes立场,亦不构成任何投资意见或建议。

信息安全

545

相关文章推荐

未登录头像

暂无评论