前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >浮点数精度问题透析:小数计算不准确+浮点数精度丢失根源

浮点数精度问题透析:小数计算不准确+浮点数精度丢失根源

原创
作者头像
周陆军
发布于 2019-05-17 06:07:19
发布于 2019-05-17 06:07:19
3K00
代码可运行
举报
文章被收录于专栏:前端架构前端架构
运行总次数:0
代码可运行

在知乎上上看到如下问题:

浮点数精度问题的前世今生?

1.该问题出现的原因 ?

2.为何其他编程语言,比如java中可能没有js那么明显

3.大家在项目中踩过浮点数精度的坑?

4.最后采用哪些方案规避这个问题的?

5.为何采用改方案?

例如在 chrome js console 中:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
alert(0.7+0.1); //输出0.7999999999999999

之前自己答的不是满意(对 陈嘉栋的回答 还是满意的),想对这个问题做个深入浅出的总结

再看到这几篇长文《[ JS 基础 ] JS 浮点数四则运算精度丢失问题 (3)》、《JavaScript数字精度丢失问题总结》、《细说 JavaScript 七种数据类型》,略有所悟,整理如下:

这个问题并不只是在Javascript中才会出现,任何使用二进制浮点数的编程语言都会有这个问题,只不过在 C++/C#/Java 这些语言中已经封装好了方法来避免精度的问题,而 JavaScript 是一门弱类型的语言,从设计思想上就没有对浮点数有个严格的数据类型,所以精度误差的问题就显得格外突出。

浮点数丢失产生原因

JavaScript 中的数字类型只有 Number 一种,Number 类型采用 IEEE754 标准中的 “双精度浮点数” 来表示一个数字,不区分整数和浮点数 (js位运算或许是为了提升B格)。

几乎所有的编程语言浮点数都是都采用IEEE浮点数算术标准。java float 32 浮点数:  1bit符号  8bit指数部分 23bit尾数。推荐阅读《JAVA 浮点数的范围和精度

什么是IEEE-745浮点数表示法

IEEE-745浮点数表示法是一种可以精确地表示分数的二进制示法,比如1/2,1/8,1/1024

十进制小数如何表示为转为二进制
十进制整数转二进制

十进制整数换成二进制一般都会:1=>1 2=>10 3=>101 4=>100 5=>101 6=>110   

6/2=3…0

3/2=1…1

1/2=0…1

倒过来就是110

十进制小数转二进制

0.25的二进制

0.25*2=0.5 取整是0

0.5*2=1.0    取整是1

即0.25的二进制为 0.01 ( 第一次所得到为最高位,最后一次得到为最低位)

0.8125的二进制

0.8125*2=1.625   取整是1

0.625*2=1.25     取整是1

0.25*2=0.5       取整是0

0.5*2=1.0        取整是1

即0.8125的二进制是0.1101(第一次所得到为最高位,最后一次得到为最低位)

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.4*2=0.8======取出整数部分0

0.8*2=1.6======取出整数部分1

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

所以0.1转化成二进制是:0.0001 1001 1001 1001…(无限循环)

0.1 => 0.0001 1001 1001 1001…(无限循环)

同理0.2的二进制是0.0011 0011 0011 0011…(无限循环)

IEEE-745浮点数表示法存储结构

在 IEEE754 中,双精度浮点数采用 64 位存储,即 8 个字节表示一个浮点数 。其存储结构如下图所示:

指数位可以通过下面的方法转换为使用的指数值:

IEEE-745浮点数表示法记录数值范围

从存储结构中可以看出, 指数部分的长度是11个二进制,即指数部分能表示的最大值是 2047(2^11-1)

取中间值进行偏移,用来表示负指数,也就是说指数的范围是 -1023,1024 。

因此,这种存储结构能够表示的数值范围为 2^1024 到 2^-1023 ,超出这个范围的数无法表示 。2^1024  和 2^-1023  转换为科学计数法如下所示:

1.7976931348623157 × 10^308

5 × 10^-324

因此,JavaScript 中能表示的最大值是 1.7976931348623157 × 10308,最小值为 5 × 10-324 。java双精度类型 double也是如此。

这两个边界值可以分别通过访问 Number 对象的 MAX_VALUE 属性和 MIN_VALUE 属性来获取:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
Number.MAX_VALUE; // 1.7976931348623157e+308
Number.MIN_VALUE; // 5e-324

如果数字超过最大值或最小值,JavaScript 将返回一个不正确的值,这称为 “正向溢出(overflow)” 或 “负向溢出(underflow)” 。 

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
Number.MAX_VALUE+1 == Number.MAX_VALUE; //true
Number.MAX_VALUE+1e292; //Infinity
Number.MIN_VALUE + 1; //1
Number.MIN_VALUE - 3e-324; //0
Number.MIN_VALUE - 2e-324; //5e-324

IEEE-745浮点数表示法数值精度

在 64 位的二进制中,符号位决定了一个数的正负,指数部分决定了数值的大小,小数部分决定了数值的精度

IEEE754 规定,有效数字第一位默认总是1 。因此,在表示精度的位数前面,还存在一个 “隐藏位” ,固定为 1 ,但它不保存在 64 位浮点数之中。也就是说,有效数字总是 1.xx...xx 的形式,其中 xx..xx 的部分保存在 64 位浮点数之中,最长为52位 。所以,JavaScript 提供的有效数字最长为 53 个二进制位,其内部实际的表现形式为:

(-1)^符号位 * 1.xx...xx * 2^指数位

这意味着,JavaScript 能表示并进行精确算术运算的整数范围为:-2^53-1,2^53-1,即从最小值 -9007199254740991 到最大值 9007199254740991 之间的范围 。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
Math.pow(2, 53)-1 ; // 9007199254740991
-Math.pow(2, 53)-1 ; // -9007199254740991

可以通过 Number.MAX_SAFE_INTEGER 和  Number.MIN_SAFE_INTEGER 来分别获取这个最大值和最小值。 

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
console.log(Number.MAX_SAFE_INTEGER) ; // 9007199254740991
console.log(Number.MIN_SAFE_INTEGER) ; // -9007199254740991

对于超过这个范围的整数,JavaScript 依旧可以进行运算,但却不保证运算结果的精度。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
Math.pow(2, 53) ; // 9007199254740992
Math.pow(2, 53) + 1; // 9007199254740992
9007199254740993; //9007199254740992
90071992547409921; //90071992547409920
0.923456789012345678;//0.9234567890123456

IEEE-745浮点数表示法数值精度丢失

计算机中的数字都是以二进制存储的,二进制浮点数表示法并不能精确的表示类似0.1这样 的简单的数字

如果要计算 0.1 + 0.2 的结果,计算机会先把 0.1 和 0.2 分别转化成二进制,然后相加,最后再把相加得到的结果转为十进制 

但有一些浮点数在转化为二进制时,会出现无限循环 。比如, 十进制的 0.1 转化为二进制,会得到如下结果:

0.1 => 0.0001 1001 1001 1001…(无限循环)

0.2 => 0.0011 0011 0011 0011…(无限循环)

而存储结构中的尾数部分最多只能表示 53 位。为了能表示 0.1,只能模仿十进制进行四舍五入了,但二进制只有 0 和 1 , 于是变为 0 舍 1 入 。 因此,0.1 在计算机里的二进制表示形式如下:

0.1 => 0.0001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 101

0.2 => 0.0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 001

用标准计数法表示如下:

0.1 => (−1)0 × 2^4 × (1.1001100110011001100110011001100110011001100110011010)2

0.2 => (−1)0 × 2^3 × (1.1001100110011001100110011001100110011001100110011010)2 

在计算浮点数相加时,需要先进行 “对位”,将较小的指数化为较大的指数,并将小数部分相应右移:

最终,“0.1 + 0.2” 在计算机里的计算过程如下:

经过上面的计算过程,0.1 + 0.2 得到的结果也可以表示为:

(−1)0 × 2−2 × (1.0011001100110011001100110011001100110011001100110100)2=>.0.30000000000000004

通过 JS 将这个二进制结果转化为十进制表示:

(-1)**0 * 2**-2 * (0b10011001100110011001100110011001100110011001100110100 * 2**-52); //0.30000000000000004

console.log(0.1 + 0.2) ; // 0.30000000000000004

这是一个典型的精度丢失案例,从上面的计算过程可以看出,0.1 和 0.2 在转换为二进制时就发生了一次精度丢失,而对于计算后的二进制又有一次精度丢失 。因此,得到的结果是不准确的。

浮点数丢失解决方案

我们常用的分数(特别是在金融的计算方面)都是十进制分数1/10,1/100等。或许以后电路设计或许会支持十进制数字类型以避免这些舍入问题。在这之前,你更愿意使用大整数进行重要的金融计算,例如,要使用整数‘分’而不是使用小数‘元’进行货比单位的运算

即在运算前我们把参加运算的数先升级(10的X的次方)到整数,等运算完后再降级(0.1的X的次方)。

在java里面有BigDecimal库,js里面有big.js js-big-decimal.js。当然BCD编码就是为了十进制高精度运算量制。

BCD编码

BCD编码(一般指8421BCD码形式)亦称二进码十进数或二-十进制代码。用4位二进制数来表示1位十进制数中的0~9这10个数。一般用于高精度计算。比如会计制度经常需要对很长的数字串作准确的计算。相对于一般的浮点式记数法,采用BCD码,既可保存数值的精确度,又可免去使电脑作浮点运算时所耗费的时间。

为什么采用二进制
  1. 二进制在电路设计中物理上更易实现,因为电子器件大多具有两种稳定状态,比如晶体管的导通和截止,电压的高和低,磁性的有和无等。而找到一个具有十个稳定状态的电子器件是很困难的。
  2. 二进制规则简单,十进制有55种求和与求积的运算规则,二进制仅有各有3种,这样可以简化运算器等物理器件的设计。另外,计算机的部件状态少,可以增强整个系统的稳定性。
  3. 与逻辑量相吻合。二进制数0和1正好与逻辑量“真”和“假”相对应,因此用二进制数表示二值逻辑显得十分自然。
  4. 可靠性高。二进制中只使用0和1两个数字,传输和处理时不易出错,因而可以保障计算机具有很高的可靠性

我觉得主要还是因为第一条。如果比如能够设计出十进制的元器件,那么对于设计其运算器也不再话下。

JS数字精度丢失的一些典型问题

两个简单的浮点数相加

0.1 + 0.2 != 0.3 // true

toFixed 不会四舍五入(Chrome)

1.335.toFixed(2) // 1.33

再问问一个问题 :在js数字类型中浮点数的最高精度多少位小数?(16位 or 17位?……why?

  1. JavaScript 能表示并进行精确算术运算的整数范围为:-2^53-1,2^53-1,即从最小值 -9007199254740991 到最大值 9007199254740991 之间的范围。'9007199254740991'.length//16 
  2. IEEE754 规定,有效数字第一位默认总是1 。因此,在表示精度的位数前面,还存在一个 “隐藏位” ,固定为 1 ,但它不保存在 64 位浮点数之中。也就是说,有效数字总是 1.xx...xx 的形式,其中 xx..xx 的部分保存在 64 位浮点数之中,最长为52位 。所以,JavaScript 提供的有效数字最长为 53 个二进制位

let a=1/3

a.toString();//"0.3333333333333333"

a.toString();.length//18

a*3===0.3333333333333333*3===1

0.3333333333333332*3!==1

相关链接:  

http://0.30000000000000004.com

http://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html

浮点数精度问题透析:小数计算不准确+浮点数精度丢失根源 - computer science - 周陆军的个人网站 如有不妥之处,请到本人源站留言。不是更新。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
暂无评论
推荐阅读
无显示器玩转树莓派桌面版
zhaoolee在Github开启了长篇连载《树莓派不吃灰》https://github.com/zhaoolee/pi 目前已经更新到18篇,主要是给树莓派刷Ubuntu当做家庭服务器用。
zhaoolee
2023/07/11
6090
无显示器玩转树莓派桌面版
树莓派构建无线打印服务器
如果没有你的打印机驱动也不要紧,上网搜搜PPD文件来告诉CUPS如何配置你的打印机即可。这个文件,我也没有测试过,可以肯定的是有这个东西~~~,如果你编译不了,自带的驱动也没有你的型号,可以测试~~~~~~
云深无际
2021/04/14
3.6K0
树莓派构建无线打印服务器
打印机+树莓派=网络打印机?
  之前买了台pi zero,然后一直仍在家里当网站服务器,感觉有点浪费。正好家里有台打印机,于是找了根OTG线连接打印机实现局域网内共享打印机。
xcsoft
2021/07/14
3.6K1
Linux下用CUPS的打印机服务
目前的Linux发行版本基本上都使用cups作为Linux下管理打印的服务应用。CUPS软件为Unix/Linux用户提供了有效而可靠的方式来管理打印的方法。它生来就支持IPP,并有LPD、SMB和JetDirect接口。CUPS本身可以提供网络打印机功能,使用它可以非常方便的令Linux与Linux之间、Linux与Windows之间实现打印共享。
用户1685462
2021/07/28
4.3K0
树莓派:漂洋过海来看你
给树莓派连上显示器和键盘鼠标,就可以像使用一台电脑一样使用它了。但很多时候,我们是把体积小巧的树莓派当做一个便携设备来使用的。这种时候,用户可不希望随身带着体积庞大的鼠标、键盘和显示器。如果能用手中的
Vamei
2018/01/18
2.5K0
树莓派:漂洋过海来看你
《树莓派4B家庭服务器搭建指南》第十七期:树莓派配合性能更好的闲置笔记本搭建私人影院
Jellyfin是一款开源免费的私人影院(影音媒体管理)系统,可以帮我们把硬盘里的影视资源管理起来,并添加精美的海报.
zhaoolee
2023/07/11
1.5K0
《树莓派4B家庭服务器搭建指南》第十七期:树莓派配合性能更好的闲置笔记本搭建私人影院
将树莓派3B刷成OpenWrt软路由,成为魔法WiFi上网的强大路由器
今天刷油管,我看到了大量带货软路由的视频,心动不已,但看了售价后,我决定让树莓派3B重出江湖!
zhaoolee
2021/03/02
18.1K2
将树莓派3B刷成OpenWrt软路由,成为魔法WiFi上网的强大路由器
利用树莓派搭建超级路由器
我们生活中常见的都是一些比较常见的家庭的路由器。而我又喜欢动手DIY,可又苦于没有钱😂。想玩一些比较高级的东西只能望洋兴叹!!! image.png 准备 树莓派4b Openwrt镜像(文末附下载链接) TF卡 读卡器 写入系统 利用Win32DiskImager将下载下来的openwrt镜像写入TF卡。 image.png 启动 将TF卡插入树莓派。通电即可! 登录 用网线连接pc和树莓派,浏览器访问192.168.1.1看到登录页面,说明写入成功。 image.png 用户名:root 密码:pass
逍遥子大表哥
2021/12/19
6320
利用树莓派搭建超级路由器
利用Ubuntu主机搭建共享打印服务
实验室的打印机自带的无线打印功能不太好用, 基本上大家都处于一种时断时续的薛定谔状态, 惠普smart一次又一次的用行动证明了这玩意实在不是很smart, 所以用 linux 搭建一个共享打印机服务或许是个不错的选择.
叶子Tenney
2023/04/05
6.1K0
利用Ubuntu主机搭建共享打印服务
如何快速实现异地不同网络打印机共享
内网打印机的不同电脑共享比较简单,但是工作生活中经常会出现不同局域网的打印机需要共享的情况,下面我们通过一个特殊的办法实现异地局域网共享打印机。
用户5084575
2019/04/23
2.9K0
如何快速实现异地不同网络打印机共享
《树莓派4B家庭服务器搭建指南》第十八期:代理Windows台式机支持Remote Desktop外网远程桌面连接, 随时玩转Stable Diffusion WebUI
最近几天, zhaoolee在家中Windows台式机折腾Stable Diffusion WebUI , 为了出门在外也能访问Windows台式机的Stable Diffusion WebUI, 我打算用树莓派代理台式机的3389端口,将其映射到公网上,以便随时访问Windows台式机(文末有Stable Diffusion出的图)。
zhaoolee
2023/07/11
1.1K0
《树莓派4B家庭服务器搭建指南》第十八期:代理Windows台式机支持Remote Desktop外网远程桌面连接, 随时玩转Stable Diffusion WebUI
打印机安全研究(一):不容乐观的网络打印机安全状况
打印机是人们在生活和办公中经常使用的电子设备,家庭、办公室、公司、政府单位、医院、学校......几乎每一个单位和机构都会使用打印机。从安全的角度来看,由于打印设备部署于内部网络,通过它们可以直接访问
FB客服
2018/02/09
2.8K0
打印机安全研究(一):不容乐观的网络打印机安全状况
树莓派4B如何手动固定IP地址
在使用树莓派的过程中,DHCP往往会自动分配树莓派的IP,因此树莓派的IP地址并不是固定的,那么每次在远程登录树莓派前都需要查看一下树莓派的IP地址,非常麻烦。因此,我们手动给树莓派设定一个静态IP地址后,树莓派的IP地址就是固定的了。
全栈程序员站长
2022/09/06
3.8K0
树莓派4B如何手动固定IP地址
树莓派4裸机基础教程:环境搭建
树莓派4作为一款学习嵌入式arm开发的开发板,是非常不错的选择。嵌入式开发往往需要的不仅仅是理论知识,还需要动手操作,然后实际体验效果。由于目前开发板要么资料太少,要么板子太贵,或者可玩性太低,所以嵌入式的入门和深入一直都是非常困难的问题。我写树莓派4裸机基础教程、树莓派4驱动进阶、树莓派4的RTOS这一些列的文章,也是希望借此机会,和大家分享一下嵌入式开发过程的方方面面,也希望对学习嵌入式感兴趣的人在阅读完成这些文章中会有所收获。由于树莓派4的外设,相对于前代的树莓派2、树莓派3等标准许多,完全可以作为学习嵌入式,学习arm编程的不错选择。所谓万变不离其宗,学会树莓派4的嵌入式开发,以后做其他的芯片的底层开发时,也是可以借鉴这种思想的。
bigmagic
2020/09/18
2.6K1
3个有用的树莓派网络项目
尽管树莓派是全世界电脑爱好者的挚爱伴侣,但它没有得到足够的赞誉。事实上,各种类型的单板计算机都没有得到应有的效果——我只是碰巧有一个树莓派。正是在对我树莓派所在的空间角落里匆匆瞥一眼,完成了我分配的任务,我才考虑我想的更高级的项目。
天然 8129060
2021/01/29
2K0
树莓派初次使用(史上最全最详细教程!!)「建议收藏」
由于自己电脑装虚拟机莫名的卡,所以搞个树莓派来当我的私密环境(嘿嘿,别想歪了!!!)废话不多说,我们开始吧!
全栈程序员站长
2022/11/04
2.7K0
树莓派无屏幕无线远程
同样,在SD卡的根目录(boot)新建“wpa_supplicant.conf”文件;
菜菜有点菜
2022/03/17
7420
树莓派无屏幕无线远程
利用树莓派+AdGuard屏蔽小米广告
一旦启动并运行,您可以在浏览器中输入以下内容,在端口3000上访问您的AdGuard Home Web界面 - http://192.168.10.20:3000/进行安装。
逍遥子大表哥
2021/12/17
4.1K0
利用树莓派+AdGuard屏蔽小米广告
uos访问windows共享打印机_Linux打印机安装命令
创作立场声明:个人瞎折腾,文中部分内容来自网络,本人并非专业人士,只是将个人的折腾经验分享给大家,如有错误请大家指正
全栈程序员站长
2022/11/07
6.1K0
树莓派介绍以及FAQ【这是我见过最全的树莓派教程】
树莓派是什么? 树莓派(Raspberry Pi)是尺寸仅有信用卡大小的一个小型电脑,您可以将树莓派连接电视、显示器、键盘鼠标等设备使用。 树莓派能替代日常桌面计算机的多种用途,包括文字处理、电子表格、媒体中心甚至是游戏。并且树莓派还可以播放高至 4K 的高清视频。 我们希望将树莓派推广给全世界的青少年电脑爱好者,用于培养计算机程序设计的兴趣和能力。
全栈程序员站长
2022/11/04
5K0
推荐阅读
相关推荐
无显示器玩转树莓派桌面版
更多 >
LV.2
这个人很懒,什么都没有留下~
目录
  • 浮点数精度问题的前世今生?
  • 浮点数丢失产生原因
    • 什么是IEEE-745浮点数表示法
      • 十进制小数如何表示为转为二进制
      • 十进制小数转二进制
    • IEEE-745浮点数表示法存储结构
    • IEEE-745浮点数表示法记录数值范围
    • IEEE-745浮点数表示法数值精度
    • IEEE-745浮点数表示法数值精度丢失
      • BCD编码
      • 为什么采用二进制
  • JS数字精度丢失的一些典型问题
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档