javaScript-浮点数精度
2022-01-16·5min
type
Post
summary
status
Published
category
tags
slug
date
Jan 16, 2022
password
icon
0.1+0.2是否等于0.3作为一道经典的面试题已经广为人知,基本都能回答出由于浮点数精度的缺失,结果不等于0.3。
但要究其原因才能发现问题的本质,今天就来了解一下,为什么会导致浮点数精度的缺失以及怎么解决精度缺失带来的问题。
如何储存浮点数
和我们日常使用的十进制不同,在计算机的世界中使用二进制来表达一切。
所以,所有代码都会被转换为二进制来进行处理,包括数字。
但数字有大有小,如果不遵循一种规范,通通转换为二进制不但会浪费空间,且不好处理。
为了可以归一化处理整数和小数,节省存储空间,JS的Number类型遵循IEEE754规范中的双精度浮点数来实现,也就是说,会用64位bit来储存一个Number类型。
那么要如何分配这64位bit来表示尽可能多的数字呢?
IEEE754规范将这64位长度分为三部分:
- 符号位S:第1位是正负数符号位(sign),0代表正数,1代表负数
- 指数位E:中间的 11 位存储指数(exponent),用来表示次方数
- 尾数位M:最后的 52 位是尾数(mantissa),超出的部分零舍一入
简单来说就是1位符号位+11位指数位+52位尾数位 = 64位
实际的数字可以用以下公示表示:
注意以上的公式遵循科学计数法的规范,在十进制是为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
注:
- 十进制转二进制,整数部分除以二取余,直至商为0,余数倒序排列就是二进制。
- 小数部分乘二取整,直至小数部分为0,整数顺序排列就是小数的二进制。
- 整数部分二进制与小数部分二进制组合就是完整的转换后的二进制。
浮点数精度是如何丢失的?
拿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是截断了一部分精度后得到的结果,那么这个问题就可以转化为:双精度浮点数是按什么规则来截断的呢?
引用双精度浮点数的原文:
如果一个 IEEE 754 的双精度浮点数被转成至少含17位有效数字的十进制数字字符串,当这个字符串转回双精度浮点数时,必须要跟原来的数相同;
简单来说,就是一个双精度的浮点数转为十进制的数字时,只要它转回来的双精度浮点数不变,精度取最短的那个就行。
由于0.1和0.10000000000000001转成双精度浮点数的存储是一样的,所以取最短的0.1就行了。
为什么1.005.toFixed(2)=1.00而不是1.01
这个问题其实就是因为当转换为二进制,以双精度浮点数的形式存储时,会产生精度丢失。再转回十进制时就已经有误差了。
可以用toPrecision方法在控制台看一下1.005在高精度下的返回:
很明显1.005只是一个被截断后的数字,所以进行保留2位的四舍五入时,2位后的数字会被全部舍去。