如何避开JavaScript浮点数计算精度问题(如0.1+0.2!==0.3)

更新时间:2023-07-26 19:17:36 阅读: 评论:0

如何避开JavaScript浮点数计算精度问题(如0.1+0.2!==0.3)不知道⼤家在使⽤JS的过程中有没有发现某些浮点数运算的时候,得到的结果存在 精度问题:⽐如0.1 + 0.2 =
0.30000000000000004以及7 * 0.8 = 5.6000000000000005等等。
究竟是什么原因造成了这个问题?实际上是因为计算机内部的信息都是由 ⼆进制⽅式表⽰的,即0和1组成的各种编码,但由于 某些浮点数没办法⽤⼆进制准确的表⽰出来,也就带来了⼀系列精度问题。当然这也 不是JS独有的问题。
接下来让我们 以0.1+0.2为例,深⼊理解⼀下 浮点数的运算⽅法,以及使⽤JS时应该如何 规避这个问题。这个问题很基础,但也很有了解的必要,⼤家就当是复习⼀下 《计算机组成原理》吧。
通过后⾯的⼏个⼩章节,将会⼤致为⼤家介绍以下⼏个⽅⾯内容:
● 浮点数的⼆进制表⽰⽅式
● IEEE 754 标准是什么
● 避开浮点数计算精度问题的⽅案
● 测试框架(Mocha)的基本⽤法
⼀、计算机的运算⽅式
㈠ 如何将⼩数转成⼆进制
① 整数部分:除2取余数,若商不为0则继续对它除2,当商为0时则将所有余数逆序排列;
② ⼩数部分:乘2取整数部分,若⼩数不为0则继续乘2,直⾄⼩数部分为0将取出的整数位正序排列。(若⼩数部分⽆法为零,根据有效位数要求取得相应数值,位数后⼀位0舍1⼊进⾏取舍)
利⽤上述⽅法,我们尝试⼀下将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,所以 ⼆进制是没办法精确表⽰0.1的。
那么0.1的⼆进制表⽰是: (0011⽆限循环)
⽽0.2的⼆进制表⽰则是: (0011⽆限循环)
⽽具体应该保存多少位数,则 需要根据使⽤的是什么标准来确定,也就是下⼀节所要讲到的内容。
股市休市时间㈡ IEEE 754 标准
IEEE 754 标准是IEEE⼆进位浮点数算术标准(IEEE Standard for Floating-Point Arithmetic)的标准编号。IEEE 754 标准规定了计算机程序设计环境中的⼆进制和⼗进制的浮点数⾃述的交换、算术格式以及⽅法。
根据IEEE 754标准,任意⼀个⼆进制浮点数都可以表⽰成以下形式:
S为数符,它表⽰浮点数的正负(0正1负);M为有效位(尾数);E为阶码,⽤移码表⽰,阶码的真
值都被加上⼀个常数(偏移量)。
尾数部分M通常都是规格化表⽰的,即⾮"0"的尾数其第⼀位总是"1",⽽这⼀位也称隐藏位,因为存储时候这⼀位是会被省略的。⽐如保存1.0011时,只保存0011,等读取的时候才把第⼀位的1加上去,这样做相当于多保存了1位有效数字。
常⽤的浮点格式有:
① 单精度:
这是32位的浮点数,最⾼的1位是符号位S,后⾯的8位是指数E,剩下的23位为尾数(有效数字)M;
其 真值为:
② 双精度:
这是64位的浮点数,最⾼的1位是符号位S,后⾯的11位是指数E,剩下的52位为尾数(有效数字)M;
其 真值为:
JavaScript只有⼀种数字类型number,⽽number使⽤的就是 IEEE 754双精度浮点格式。依据上述规则,接下来我们就来看看JS是如何存储0.1和0.2的:合计公式
0.1是正数,所以 符号位是0;
⽽其⼆进制位是 (0011⽆限循环),进⾏ 规格化后为1001(1)*2^-4,根据0舍1⼊的规则,最后的值为
2^-4 * 1.1001100110011001100110011001100110011001100110011010
⽽ 指数E = -4 + 1023 = 1019
由此可得,JS中 0.1的 ⼆进制存储格式为 (符号位⽤逗号分隔,指数位⽤分号分隔):
0,01111111011;1001100110011001100110011001100110011001100110011010
0.2则为:
0,01111111100;1001100110011001100110011001100110011001100110011010
Q1:指数位E(阶码)为何⽤移码表⽰?
A1:为了便于判断其⼤⼩。
㈢ 浮点数运算
0.1 => 0,01111111011;1001100110011001100110011001100110011001100110011010
0.2 => 0,01111111100;1001100110011001100110011001100110011001100110011010
浮点数的加减运算按以下⼏步进⾏:
疗休养
① 对阶,使两数的⼩数点位置对齐(也就是使两数的阶码相等)。
所以要先求阶差,阶⼩的尾数要根据阶差来右移 (尾数位移时可能会发⽣数丢失的情况,影响精度)
因为0.1和0.2的阶码和尾数均为正数,所以它们的原码、反码及补码都是⼀样的。(使⽤补码进⾏运算,计算过程中使⽤双符号)
△阶差(补码) = 00,01111111011 - 00,01111111100 = 00,01111111011 + 11,10000000100 = 11,11111111111
由上可知 △阶差为-1,也就是0.1的阶码⽐0.2的⼩,所以要把0.1的尾数右移1位,阶码加1(使0.1的阶码和0.2的⼀致)
最后0.1 => 0,01111111100;1100110011001100110011001100110011001100110011001101(0)
注:要注意0舍1⼊的原则。之所以右移⼀位,尾数补的是1,是因为隐藏位的数值为1(默认是不存储的,只有读取的时候才加上)
② 尾数求和
0.1100110011001100110011001100110011001100110011001101
+ 1.1001100110011001100110011001100110011001100110011010
——————————————————————————————
10.0110011001100110011001100110011001100110011001100111
③ 规格化
针对步骤②的结果,需要 右规(即尾数右移1位,阶码加1)
sum = 0.1 + 0.2 = 0,01111111101;1.0011001100110011001100110011001100110011001100110011 (1)
注:右规操作,可能会导致低位丢失,引起误差,造成精度问题。所以就需要步骤④的舍⼊操作
④ 舍⼊(0舍1⼊)
sum = 0,01111111101;1.0011001100110011001100110011001100110011001100110100
⑤ 溢出判断
根据阶码判断浮点运算是否溢出。⽽我们的阶码01111111101即不上溢,也不下溢。
⾄此,0.1+0.2的运算就已经结束了。接下来,我们⼀起来看看上⾯计算得到的结果,它的⼗进制数是多少。
<1> 先将它⾮规格化,得到⼆进制形式:
sum = 0.010011001100110011001100110011001100110011001100110100
<2> 再将其转成⼗进制
sum = 2^2 + 2^5 + 2^6 + ... + 2^52 = 0.30000000000000004440892098500626
现在你应该明⽩JS中 0.30000000000000004 这个结果怎么来的吧。
Q2:计算机运算为何要使⽤补码?
A2:可以简化计算机的运算步骤,且只⽤设加法器,如做减法时若能找到与负数等价的正数来代替该负数,就可以把减法操作⽤加法代替。⽽采⽤补码,就能达到这个效果。
⼆、浮点数精度问题的解决⽅法
㈠ 简单解决⽅案
我的思路就是将⼩数转成整数来运算,之后再转回⼩数。代码也⽐较简单,就直接贴出来了。
'u strict'
var accAdd = function(num1, num2) {
四去八进一打一字num1 = Number(num1);
num2 = Number(num2);
var dec1, dec2, times;
try { dec1 = countDecimals(num1)+1; } catch (e) { dec1 = 0; }
try { dec2 = countDecimals(num2)+1; } catch (e) { dec2 = 0; }
times = Math.pow(10, Math.max(dec1, dec2));
// var result = (num1 * times + num2 * times) / times;
var result = (accMul(num1, times) + accMul(num2, times)) / times;
return getCorrectResult("add", num1, num2, result);
// return result;
};
var accSub = function(num1, num2) {
num1 = Number(num1);
num2 = Number(num2);
var dec1, dec2, times;
try { dec1 = countDecimals(num1)+1; } catch (e) { dec1 = 0; }
try { dec2 = countDecimals(num2)+1; } catch (e) { dec2 = 0; }
帅哥头像阳光帅气times = Math.pow(10, Math.max(dec1, dec2));
// var result = Number(((num1 * times - num2 * times) / times);
var result = Number((accMul(num1, times) - accMul(num2, times)) / times);    return getCorrectResult("sub", num1, num2, result);
// return result;
};
var accDiv = function(num1, num2) {
num1 = Number(num1);
num2 = Number(num2);
var t1 = 0, t2 = 0, dec1, dec2;
try { t1 = countDecimals(num1); } catch (e) { }
try { t2 = countDecimals(num2); } catch (e) { }
dec1 = convertToInt(num1);
dec2 = convertToInt(num2);
var result = accMul((dec1 / dec2), Math.pow(10, t2 - t1));
return getCorrectResult("div", num1, num2, result);
// return result;
};
var accMul = function(num1, num2) {
num1 = Number(num1);
num2 = Number(num2);
var times = 0, s1 = String(), s2 = String();
try { times += countDecimals(s1); } catch (e) { }
try { times += countDecimals(s2); } catch (e) { }
var result = convertToInt(s1) * convertToInt(s2) / Math.pow(10, times);
return getCorrectResult("mul", num1, num2, result);
// return result;
};
var countDecimals = function(num) {
var len = 0;
try {
num = Number(num);
var str = String().toUpperCa();
if (str.split('E').length === 2) { // scientific notation
var isDecimal = fal;
if (str.split('.').length === 2) {
str = str.split('.')[1];
if (parInt(str.split('E')[0]) !== 0) {
isDecimal = true;
}
}
let x = str.split('E');
if (isDecimal) {
馅饼里包了一块天
len = x[0].length;
}
len -= parInt(x[1]);
} el if (str.split('.').length === 2) { // decimal
if (parInt(str.split('.')[1]) !== 0) {
len = str.split('.')[1].length;
}
}
} catch(e) {
throw e;
} finally {
if (isNaN(len) || len < 0) {
len = 0;
len = 0;
}
return len;
}
};
var convertToInt = function(num) {
num = Number(num);
var newNum = num;
var times = countDecimals(num);
var temp_num = String().toUpperCa();
if (temp_num.split('E').length === 2) {
newNum = und(num * Math.pow(10, times));
} el {
newNum = Number(place(".", ""));
}
return newNum;
};
var getCorrectResult = function(type, num1, num2, result) {
var temp_result = 0;
switch (type) {
ca "add":
temp_result = num1 + num2;
break;
ca "sub":
temp_result = num1 - num2;
break;
ca "div":
temp_result = num1 / num2;
break;
ca "mul":
temp_result = num1 * num2;
break;
}
if (Math.abs(result - temp_result) > 1) {
return temp_result;如何画孙悟空
}
return result;
};
基本⽤法:
加法: accAdd(0.1, 0.2)  // 得到结果:0.3
减法: accSub(1, 0.9)    // 得到结果:0.1
除法: accDiv(2.2, 100)  // 得到结果:0.022
乘法: accMul(7, 0.8)    // 得到结果:5.6
countDecimals()⽅法:计算⼩数位的长度
convertToInt()⽅法:将⼩数转成整数
getCorrectResult()⽅法:确认我们的计算结果⽆误,以防万⼀
㈡ 使⽤特殊进制类型类库
如果你需要⾮常精确的结果,可以考虑使⽤特殊的进制数据类型,如下⾯的这个叫bignumber的类库:

本文发布于:2023-07-26 19:17:36,感谢您对本站的认可!

本文链接:https://www.wtabcd.cn/fanwen/fan/82/1118673.html

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。

标签:浮点数   运算   问题   进制   尾数   精度   阶码
相关文章
留言与评论(共有 0 条评论)
   
验证码:
推荐文章
排行榜
Copyright ©2019-2022 Comsenz Inc.Powered by © 专利检索| 网站地图