为什么 JavaScript 中 0.1+0.2 不等于 0.3 ?
本文首发于 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中会出现,在所有采用该标准的语言中都会存在,实际编程中可以通过设置精度保留位数等方式解决。
低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
好程序员Java教程分享JavaScript常见面试题四
好程序员Java教程分享JavaScript常见面试题四1、下面的代码将输出什么到控制台,为什么? console.log(1 + "2" + "2");console.log(1 + +"2" + "2");console.log(1 + -"1" + "2");console.log(+"1" + "1" + "2");console.log( "A" - "B" + "2");console.log( "A" - "B" + 2); 上面的代码将输出以下内容到控制台: "122""32""02""112""NaN2"NaN 原因是… 这里的根本问题是,JavaScript(ECMAScript)是一种弱类型语言,它可对值进行自动类型转换,以适应正在执行的操作。让我们通过上面的例子来说明这是如何做到的。 例1:1 + "2" + "2" 输出:"122" 说明: 1 + "2" 是执行的第一个操作。由于其中一个运算对象("2")是字符串,JavaScript会假设它需要执行字符串连接,因此,会将 1 的类型转换为 "1", 1 + "2"结果就是 "12"。然后, "12" + "...
- 下一篇
对话程序员:各编程语言十年来的发展史
过去十年,编程语言发生了很大的变化,随着互联网大时代的迈进,将来还会发生更多的变化。在最新发布的IEEE年度榜单中,Python仍占据首位。 我们都非常喜欢自己的设备。手机上的相机能够帮助我们记录人生中最宝贵的时刻,而互联网及其众多通信工具能够让我们与远方的朋友和家人保持联系。通常我们都不会想起为我们带来互联网的正是各种编程语言,但是我们也不能忽视最近某些编程语言的流行发生了变化。我们可以从这些变化中了解数字世界的发展方向,同时也可以回顾一下这段发展史。 排名第一:Python 上个月,IEEE综览(IEEE Spectrum)发布了各大编程语言的年度排名,许多人惊讶地发现Python保住了头把交椅的宝座。Python这种基于脚本的编程语言,一直在IEEE综览的排名中居首位。然而,这并不奇怪。IEEE对各大编程语言的评分进行了加权处理。去年,Python的得分为100、C++的得分为99.7、Java的得分为97.5、而C的得分为96.7。然而,今年编程语言的格局发生了变化。虽然Python仍以100分高居榜首,但Java成了第二名,得分却只有96.3——大幅下降。第三位的C为94.4...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- Docker使用Oracle官方镜像安装(12C,18C,19C)
- CentOS7,CentOS8安装Elasticsearch6.8.6
- Jdk安装(Linux,MacOS,Windows),包含三大操作系统的最全安装
- Linux系统CentOS6、CentOS7手动修改IP地址
- CentOS7安装Docker,走上虚拟化容器引擎之路
- CentOS7编译安装Cmake3.16.3,解决mysql等软件编译问题
- SpringBoot2全家桶,快速入门学习开发网站教程
- CentOS7设置SWAP分区,小内存服务器的救世主
- Docker快速安装Oracle11G,搭建oracle11g学习环境
- CentOS7编译安装Gcc9.2.0,解决mysql等软件编译问题