为什么-JavaScript-中-0102-不等于-03

39次阅读

共计 2332 个字符,预计需要花费 6 分钟才能阅读完成。

本文首发于 vivo 互联网技术 微信公众号 
链接:https://mp.weixin.qq.com/s/2kea7-jACCJmSYBQAwXyIg
作者:刘洋

在 js 中进行数学的运算时,会出现 0.1+0.2=0.300000000000000004 的结果,一开始认为是浮点数的二进制存储导致的精度问题,但这似乎不能很好的解释为什么在同样的存储方式下 0.3+0.4=0.7 可以得到正确的结果。本文主要通过浮点数的二进制存储及运算,和 IEEE754 下的舍入规则,解释为何会出现这种情况。

一、浮点数的二进制存储

JavaScript 遵循 IEEE754 标准,在 64 位中存储一个数据的有效数字形式。

其中,第 0 位为符号位,0 表示正数 1 表示负数;第 1 到 11 位存储指数部分;第 12 到 63 位存小数部分(尾数部分)(即有效数字)。由于二进制的有效数字总是表示为 1.xxx…的形式,尾数部分在规约形式下的第一位默认为 1,故存储时第一位省略不写,尾数部分 f 存储有效数字小数点后的 xxx…,最长 52 位。因此,JavaScript 提供的有效数字最长为 53 个二进制位(尾数部分 52 位 + 被省略的 1 位)。
以 0.1、0.2、0.3、0.4 和 0.7 的二进制形式为例:

0.1->0.0001100110011...(0011 无限循环)->0-01111111011-(1 .)1001100110011001100110011001100110011001100110011010(入)
0.2->0.001100110011...(0011 无限循环)->0-01111111100-(1 .)1001100110011001100110011001100110011001100110011010(入)
0.3->0.01001100110011...(0011 无限循环)->0-01111111101-(1 .)0011001100110011001100110011001100110011001100110011(舍)
0.4->0.01100110011...(0011 无限循环)->0-01111111101-(1 .)1001100110011001100110011001100110011001100110011010(入)
0.7->0.101100110011...(0011 无限循环)->0-01111111110-(1 .)0110011001100110011001100110011001100110011001100110(舍)

对于 52 位之后进行舍入运算,此时可看作 0 舍 1 入(具体舍入规则在第三部分详细说明),有精度损失。

二、对阶运算

由于指数位数不同,运算时需要进行对阶运算。对阶过程略,0.1+0.2 与 0.3+0.4 的尾数求和结果分别如下:

0.1+0.2->10.0110011001100110011001100110011001100110011001100111
0.3+0.4->10.1100110011001100110011001100110011001100110011001101

求和结果需规格化(有效数字表示),右规导致低位丢失,此时需对丢失的低位进行舍入操作:

0.1+0.2->1.00110011001100110011001100110011001100110011001100111->1.0011001100110011001100110011001100110011001100110100(入)
0.3+0.4->1.01100110011001100110011001100110011001100110011001101->1.0110011001100110011001100110011001100110011001100110(舍)

即:
00111->0100
01101->0110

此处同样有精度损失。在这里我们可以发现,0.3+0.4 对阶阶运算且规格化后的运算结果与 0.7 在二进制中的存储尾数相同(可对照尾数后几位),而 0.1+0.2 的运算结果与 0.3 的存储尾数不同,且 0.1+0.2 转化为十进制时结果为 0.300000000000000004。
此时,虽然 0.1+0.2 与 0.3+0.4 进行舍入操作的近似位都为 1,但一入一舍导致计算结果与“标准答案”的异同。

三、IEEE754 标准下的舍入规则

维基百科对最近偶数舍入原则的解释如下:舍入到最接近,在一样接近的情况下偶数优先(Ties To Even,这是默认的舍入方式),即会将结果舍入为最接近(精度损失最小)且可以表示的值,但是当存在两个数一样接近的时候,则取其中的偶数(在二进制中是以 0 结尾的)。

首先要注意的是,保留小数不是只看后面一位或者两位,而是看保留位后面的所有位。

如图,可以看到近似需要看三位,保留位(近似后的最低位)、近似位(保留位的后一位)、粘滞位(sticky bit 近似位后的所有位进行或运算后看作一位)。
当粘滞位为 1 时,舍入规则可以看作 0 舍 1 入,近似位为 0 舍,近似位为 1 入(即第一部分小数二进制存储为 52 位尾数时所进行的舍入操作)。
当粘滞位为 0 时,若近似位为 0 则舍去。
当粘滞位为 0 时,若近似位为 1,无论舍入精度损失都相同,故需取舍入两种结果中的偶数:保留位为 1 时入,保留位为 0 时舍(即第二部分对阶运算规格化时的舍入操作)。

四、总结思考

由于 IEEE754 标准,这样的“bug”不止在 JavaScript 中会出现,在所有采用该标准的语言中都会存在,实际编程中可以通过设置精度保留位数等方式解决。

更多内容敬请关注 vivo 互联网技术  微信公众号

注:转载文章请先与微信号:labs2020 联系。

正文完
 0