Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

javaScript-浮点数精度 #15

Open
dark9wesley opened this issue Apr 21, 2021 · 0 comments
Open

javaScript-浮点数精度 #15

dark9wesley opened this issue Apr 21, 2021 · 0 comments

Comments

@dark9wesley
Copy link
Owner

前言

0.1+0.2是否等于0.3作为一道经典的面试题已经广为人知,基本都能回答出由于浮点数精度的缺失,结果不等于0.3。
但要究其原因才能发现问题的本质,今天就来了解一下,为什么会导致浮点数精度的缺失以及怎么解决精度缺失带来的问题。

如何储存浮点数

和我们日常使用的十进制不同,在计算机的世界中使用二进制来表达一切。

所以,所有代码都会被转换为二进制来进行处理,包括数字。

但数字有大有小,如果不遵循一种规范,通通转换为二进制不但会浪费空间,且不好处理。

为了可以归一化处理整数和小数,节省存储空间,JS的Number类型遵循IEEE754规范中的双精度浮点数来实现,也就是说,会用64位bit来储存一个Number类型。

那么要如何分配这64位bit来表示尽可能多的数字呢?

IEEE754规范将这64位长度分为三部分:

  1. 符号位S:第1位是正负数符号位(sign),0代表正数,1代表负数
  2. 指数位E:中间的 11 位存储指数(exponent),用来表示次方数
  3. 尾数位M:最后的 52 位是尾数(mantissa),超出的部分零舍一入

简单来说就是1位符号位+11位指数位+52位尾数位 = 64位

实际的数字可以用以下公示表示:

V = (-1)^s * 2^(E - 1023) * (M + 1)

注意以上的公式遵循科学计数法的规范,在十进制是为0<M<10,到二进制就是0<M<2。也就是说整数部分只能是1,所以可以被舍去,只保留后面的小数部分。如 4.5 转换成二进制就是100.1,科学计数法表示是1.001*2^2,舍去1后M = 001。E是一个无符号整数,因为长度是11位,取值范围是 0~2047。但是科学计数法中的指数是可以为负数的,所以再减去一个中间数1023,[0,1022]表示为负,[1024,2047] 表示为正。如4.5 的指数E = 1025,尾数M为001。

举个例子:

如果是5.5这个数字的话,则计算过程是这样的:
5.5 转二进制: 101.1
科学计数法: 1.011*2^2 
存入计算机: 
符号位:正数存0 
指数位:E = 2 + 1023 =====> 1025 转二进制 =====> 10000000001 
尾数位:1.011 隐去小数点左边的1 =====> 011

注:

  1. 十进制转二进制,整数部分除以二取余,直至商为0,余数倒序排列就是二进制。
  2. 小数部分乘二取整,直至小数部分为0,整数顺序排列就是小数的二进制。
  3. 整数部分二进制与小数部分二进制组合就是完整的转换后的二进制。

浮点数精度是如何丢失的?

拿0.1与0.2这两个数来举例。

0.1的存储过程:

0.1转换为二进制:0.00011001100....(无限1100循环小数)
科学计数法: 1.1001100... * 2^-4
存入计算机:
符号位:正数存0 
指数位:E = -4 + 1023 =====> 1019 转二进制 =====> 1111111011 
尾数位:1.1001100... 隐去小数点左边的1 =====> 100110011001100...

可以发现由于转换为二进制后是无限循环的小数,52位的尾数为了进行储存只能在尾部对其进行舍0进1的截断。

所以当浮点数转换为二进制储存时就已经发生了精度缺失。

对于0.2的存储过程也是一样的,这里就不再赘述了。

回到0.1 + 0.2 != 0.3这道题。由于在实际存储的过程中两个数转换为二进制后是无限循环的小数,所以会被截断存储,这就产生了精度缺失。

两个精度缺失的二进制数相加后的结果也会是不准确的,所以结果转换为十进制就不等于0.3了。

一些深入问题

为什么num = 0.1能得到0.1

既然0.1在存储的时候会发生精度丢失,那为什么num = 0.1能得到0.1呢?

其实0.1不是真的0.1,可以使用toPrecision方法在控制台看一下0.1在不同精度下的返回:

0.1.toPrecision(16) //0.1000000000000000
0.1.toPrecision(17) //0.10000000000000001
0.1.toPrecision(18) //0.100000000000000006
0.1.toPrecision(19) //0.1000000000000000056
0.1.toPrecision(20) //0.10000000000000000555

可以看出来其实0.1是截断了一部分精度后得到的结果,那么这个问题就可以转化为:双精度浮点数是按什么规则来截断的呢?

引用双精度浮点数的原文:

如果一个 IEEE 754 的双精度浮点数被转成至少含17位有效数字的十进制数字字符串,当这个字符串转回双精度浮点数时,必须要跟原来的数相同;

简单来说,就是一个双精度的浮点数转为十进制的数字时,只要它转回来的双精度浮点数不变,精度取最短的那个就行。

由于0.1和0.10000000000000001转成双精度浮点数的存储是一样的,所以取最短的0.1就行了。

为什么1.005.toFixed(2)=1.00而不是1.01

这个问题其实就是因为当转换为二进制,以双精度浮点数的形式存储时,会产生精度丢失。再转回十进制时就已经有误差了。

可以用toPrecision方法在控制台看一下1.005在高精度下的返回:

1.005.toPrecision(17) //1.0049999999999999

很明显1.005只是一个被截断后的数字,所以进行保留2位的四舍五入时,2位后的数字会被全部舍去。

Number.MAX_VALUE和Number.MAX_SAFE_INTEGER如何得来?

Number.MAX_SAFE_INTEGER与Number.MAX_VALUE引发的思考

参考

JavaScript 浮点数陷阱及解法
前端应该知道的JavaScript浮点数和大数的原理

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant