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

作者 : 开心源码 本文共5508个字,预计阅读时间需要14分钟 发布时间: 2022-05-12 共202人阅读

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

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

1.该问题出现的起因 ?

2.为何其余编程语言,比方java中可能没有js那么显著

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

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

5.为何采用改方案?

例如在 chrome js console 中:

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 属性来获取:

Number.MAX_VALUE;?//?1.7976931348623157e+308

Number.MIN_VALUE;?//?5e-324

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

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 之间的范围?。

Math.pow(2,?53)-1?;?//?9007199254740991

-Math.pow(2,?53)-1?;?//?-9007199254740991

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

console.log(Number.MAX_SAFE_INTEGER)?;?//?9007199254740991

console.log(Number.MIN_SAFE_INTEGER)?;?//?-9007199254740991

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

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.jsjs-big-decimal.js。当然BCD编码就是为了十进制高精度运算量制。

BCD编码

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

为什么采用二进制

二进制在电路设计中物理上更易实现,由于电子器件大多具备两种稳固状态,比方晶体管的导通和截止,电压的高和低,磁性的有和无等。而找到一个具备十个稳固状态的电子器件是很困难的。

二进制规则简单,十进制有55种求和与求积的运算规则,二进制仅有各有3种,这样可以简化运算器等物理器件的设计。另外,计算机的部件状态少,可以加强整个系统的稳固性。

与逻辑量相吻合。二进制数0和1正好与逻辑量“真”和“假”相对应,因而用二进制数表示二值逻辑显得十分自然。

可靠性高。二进制中只使用0和1两个数字,传输和解决时不易出错,因此可以保障计算机具备很高的可靠性

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

JS数字精度丢失的少量典型问题

两个简单的浮点数相加

0.1 + 0.2 != 0.3 // true

toFixed 不会四舍五入(Chrome)

1.335.toFixed(2) // 1.33

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

JavaScript 能表示并进行准确算术运算的整数范围为:[-2^53-1,2^53-1],即从最小值 -9007199254740991 到最大值 9007199254740991 之间的范围。’9007199254740991′.length//16?

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 – 周陆军的个人网站 如有不妥之处,请到本人源站留言。不是升级。

说明
1. 本站所有资源来源于用户上传和网络,如有侵权请邮件联系站长!
2. 分享目的仅供大家学习和交流,您必须在下载后24小时内删除!
3. 不得使用于非法商业用途,不得违反国家法律。否则后果自负!
4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解!
5. 如有链接无法下载、失效或广告,请联系管理员处理!
6. 本站资源售价只是摆设,本站源码仅提供给会员学习使用!
7. 如遇到加密压缩包,请使用360解压,如遇到无法解压的请联系管理员
开心源码网 » 浮点数精度问题透析:小数计算不精确+浮点数精度丢失根源

发表回复