最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 硬核基础二进制篇(一)0.1 + 0.2 != 0.3 和 IEEE-754 标准

    正文概述 掘金(FESKY)   2021-03-17   847

    准备写几篇比较硬核的文章来说说 Javascript 中二进制相关的基础知识。这是第一篇 《IEEE-754 标准和浮点数运算》,通读全文你可以掌握 Number 在 Javascript 中是如何存储的,Number.MAX_SAFE_INTERGE 是怎么来的,以及非常常见的面试题,为什么 0.1 + 0.2 !== 0.3

    Javascript 是怎么存储数字的 —— IEEE-754 标准

    JavaScript 的数字是 IEEE-754 标准存储的双精度浮点数类型。双精度浮点数总共有 64 位(bit),第一位用于表示符号,接着十一位用于表示阶码,剩余的五十二位用于表示尾数。

    硬核基础二进制篇(一)0.1 + 0.2 != 0.3 和 IEEE-754 标准

    符号位很好理解,0 表示正数,1 表示负数。阶码和尾数表示什么呢?IEEE-754 标准中,一个浮点数将被使用二进制科学计数法的方式存储。看下面的公式:

    硬核基础二进制篇(一)0.1 + 0.2 != 0.3 和 IEEE-754 标准

    阶码(exponent)

    表示的是二的多少次方,范围是 -1024~1023

    阶码是使用 移码 表示法存储的(维基百科中文上说阶码使用的是补码表示法,是错误的,不信你看英文版),偏移值为 +1023,也就是在阶码运算时需要在二进制运算的基础上,手动减去 1023 才是真正表达的值。

    01111111111 // 0
    10000000000 // 1
    11111111110 // 1023
    00000000000 // -1024
    

    尾数 (mantissa)

    你可能注意到了,根据公式尾码表示的是小数点后面的部分,整数部分永远是 1。这是因为对于任意一个非零数字,第一位有效数字肯定是 1 嘛。所以标准规定,第一个 1 不需要存

    首先,尾码存的是二进制小数的部分,我们需要先弄清楚二进制是怎么表示小数的?

    先看十进制的小数是怎么算的:

    (0.625)10 = 0 + 6 * 10^-1 + 2 * 10^-2 + 5*10^-3
    

    可以看到十进制小数点后第 n 个数字是该数字乘以基数(10)的 -n 次方,二进制格式也是一样的道理,只是基数变成 2。

    (0.101)2 => 0 + 2^-1 + 2^-3 => 十进制的 1/2 + 1/8 = 0.625
    

    第二个问题,永远有个隐藏的 1,那数字 0 是怎么表示呢?由于阶码表示的是二的多少次方,所表示的数字非常大,大到隐藏的 1 可以忽略不计。根据标准 0 的二进制形式为:

    0 00000000000 0000000000000000000000000000000000000000000000000000
    

    换算成十进制为 2^-1024 * 1.0,是一个小到可以忽略不计的极小值。

    Number.MAX_SAFE_INTEGER 是怎么来的

    尾数位数决定了最大的整数范围,在做数值运算时,我们会要求数值以及运算结果必须不能超出 -Number.MAX_SAFE_INTEGER ~ Number.MAX_SAFE_INTEGER 的范围。

    首先这个范围是怎么来的,尾数总共 52 位,加上不存的 1 总共 53 位,所能表达的最大值是 53 位全是 1

    111...111(53 个 1) = 1000...000(53 个 0) - 1 也就是 2^53 - 1

    硬核基础二进制篇(一)0.1 + 0.2 != 0.3 和 IEEE-754 标准

    超出范围的时候发生了什么?尾码只有 52 位,放不下的部分就溢出了呗,这时候 UnSafe 的情况就出现了。看下面的例子:

    9007199254740992 的二进制形式为:

    0 10000110100 0000000000000000000000000000000000000000000000000000
    

    9007199254740993 的二进制形式也是!!!

    0 10000110100 0000000000000000000000000000000000000000000000000000
    

    所以如果拿这两个数字判断是否相等,结果当然是 true!!!

    硬核基础二进制篇(一)0.1 + 0.2 != 0.3 和 IEEE-754 标准

    同样的问题还会发生在小数运算,最常见的问题,也是常被拿出来放在面试过程问的,为什么 0.1 + 0.2 !== 0.3

    0.1 + 0.2 !== 0.3

    看一下计算机是怎么存储十进制的 0.1 的。

    如果是整数,十进制转二进制我们是通过除二取余,十进制 15 转二进制的过程:

    15 % 2 === 1, 15 => 7
    7 % 2 === 1, 15 => 3
    3 % 2 === 1, 3 => 1
    1 % 2 === 1, 1 => 0
    

    得到 15 的二进制形式 1111。小数部分的计算规则和整数不一样,使用的方式是乘二取整法,小数 0.125 转成二进制的过程:

    0.125 * 2 => 0.25, 0
    0.25 * 2 => 0.5, 0
    0.5 * 2 => 1, 1
    

    十进制 0.125 的二进制形式是 0.001 (2^-3)。

    掌握了小数的二进制表示法,来看一下十进制 0.1 用二进制怎么表示。

    0.1 * 2 => 0.2, 0
    0.2 * 2 => 0.4, 0
    0.4 * 2 => 0.8, 0
    0.8 * 2 => 1.6, 1
    0.6 * 2 => 1.2, 1
    0.2 * 2 => 0.4, 0
    ...
    

    可以看到,0.1 用二进制表示为 0.00011001100110011..., 出现了 0011 的无限循环,第一个有效数字1出现在小数点后第四位,把它往前移动四位,阶码为 -4,符号位为 0,合在一起就得到 0.1 的二进制形式:

    0 01111111011 1001100110011001100110011001100110011001100110011010
    

    0.20.1 的两倍,尾码保持不动,阶码 + 1,得到 0.2 的二进制形式:

    0 01111111100 1001100110011001100110011001100110011001100110011010
    

    到这里,我们知道了,数字 0.10.2 在计算机里表示的时候,本身就存在精度丢失(本来无限循环的数字被截断了)。再拿这两个数做加法,看看会是什么结果。

    0.2 的阶码比 0.1 的阶码大一,我们把 0.1 的尾码右移一位,阶码减 1,让两个数的阶码保持一致

    0 01111111100 0.1100110011001100110011001100110011001100110011001101
    0 01111111100 1.1001100110011001100110011001100110011001100110011010
    

    现在阶码相同了,尾数相加得到:

      0 01111111100  0.1100110011001100110011001100110011001100110011001101
    + 0 01111111100  1.1001100110011001100110011001100110011001100110011010
    = 0 01111111100 10.0110011001100110011001100110011001100110011001100111
    

    把运算结果按照 IEEE-754 标准格式化,需要向右移动一位,阶码加一。但这时候发现,最后一个 1 放不下了,需要舍弃,根据标准当要舍弃一位数时,需要进行0舍1入。如果被舍弃的是 0 什么都不用做,如果被舍弃的是1,则需要补回来。

    0 01111111101 0011001100110011001100110011001100110011001100110011 1(1 多出,需要舍弃)
    0 01111111101 0011001100110011001100110011001100110011001100110100  (补 1)
    

    于是,我们得到了 0.1 + 0.2 的运算结果。

    0 01111111101 0011001100110011001100110011001100110011001100110100
    

    再来,使用乘2取整的方法,算一下 0.3 的二进制是怎么表示的。

    0.3 * 2 => 0.6, 0
    0.6 * 2 => 1.2, 1
    0.2 * 2 => 0.4, 0
    0.4 * 2 => 0.8, 0
    0.8 * 2 => 1.6, 1
    0.6 * 2 => 1.2, 1
    ...
    

    一样出现了循环 0011,第一个数字 1 出现在第二位,尾码往前移动两位,阶码为 -2。所以 0.3 的二进制形式如下:

    0 01111111101 0011001100110011001100110011001100110011001100110011
    

    0.1 + 0.2 的运算结果确实不相等,至此我们总算搞明白了,在浮点数运算过程中的误差问题。总结一下就是,小数在计算机的存储过程中本身就存在精度丢失的问题,然后尾数的位数总共只有 52 位,放不下时会被丢弃,并按照 舍0补1 来弥补导致最终运算结果不相等。

    总结

    这篇文章到这里就结束了,水平有限难免有纰漏,欢迎纠错。下一篇文章来讲讲 Javascript 中的位运算。

    补充:浮点数运算误差不是 JavaScript 特有,所有遵循 IEEE-754 标准的实现都存在同样的问题。

    相关链接

    • babbage.cs.qc.cuny.edu/IEEE-754.ol…
    • en.wikipedia.org/wiki/Double…
    • developer.mozilla.org/zh-CN/docs/…

    起源地下载网 » 硬核基础二进制篇(一)0.1 + 0.2 != 0.3 和 IEEE-754 标准

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元