网上已经有太多的文章介绍这一块了,这里做一下记录
经典问题
0.1+0.2=0.30000000000000004
相信大家都了解了这是一个浮点数运算时的精度丢失问题。再深入了解一点的同学大概知道二进制的概念
大多数语言中的小数默认都是遵循 IEEE 754 的 float 浮点数,包括 Java、Ruby、Python,浮点数问题同样存在。
计算机所有的数据最终都是以二进制的形式存储的,计算机去计算0.1+0.2的时候,实际上计算的是这两个数字对应的二进制值运算,通过在线运算可以看到0.1:0.00011001100110011001100(1100...)
,0.2: 0.0011001100110011001100(1100...)
,既然是无限循环的,计算机肯定无法完全存储,那是怎么处理的呢?相信大家查阅资料后会知道,计算机会用有限的单位存储数据,那么就意味着舍弃部分值,这就会造成精度丢失,这也是0.1+0.2!=0.3的原因。
背后基础
这篇文章旨在分析底层数据存储问题来加深这个记忆,并且了解下面一些值的由来。比如
- 最大值:Number.MAX_VALUE => 1.7976931348623157e+308
- 最小值:Number.MIN_VALUE => 5e-324
- 最大安全整数:Number.MAX_SAFE_INTEGER => 9007199254740991
- 最小安全整数:Number.MIN_SAFE_INTEGER => -9007199254740991
补充知识点
十进制整数转二进制
十进制整数转换为二进制整数采用"除2取余,逆序排列"法。具体做法是:用2去除十进制整数,可以得到一个商和余数;再用2去除商,又会得到一个商和余数,如此进行,直到商为零时为止,然后把先得到的余数作为二进制数的低位有效位,后得到的余数作为二进制数的高位有效位,依次排列起来。
十进制小数转换为二进制小数
十进制小数转换成二进制小数采用"乘2取整,顺序排列" 法。具体做法是:用2乘十进制小数,可以得到积,将积的整数部分取出,再用2乘余下的小数 部分,又得到一个积,再将积的整数部分取出,如此进行,直到积中的小数部分为零,或者达到所要求的精度为止。(菜鸟)
底层数据存储
这个就是最重要的一点了,了解了数据的存储方式,我们所有的问题都迎刃而解了
我们知道,JS中的Number类型使用的是双精度浮点型,也就是其他语言中的double类型。而双精度浮点数使用64 bit来进行存储的,就像上图所示
- 符号位:1位,用来表示正负数,0表示正数,1表示负数
- 指数部分:11位,用来表示指数
- 尾数部分:52位,用来表示精确度
类型划分
根据实际数值类型我们也知道数值分为3种,标准数值,0(很奇怪吧,这个为什么拿出来),无穷大(还有NaN)
依据11位指数,我们来区分一下3种情况
- 11位指数不为00000000000和11111111111,即在00000000001 ~ 11111111110(1 ~ 2046)范围,这被称为规格化。
- 指数值为00000000000(0),这被称为非规格化
- 指数值为11111111111(2047),这是特殊值,有两种情况:
- 当52位小数部分f全为0时,若符号位是0,则表示+Infinity(正无穷),若符号位是1,则表示-Infinity(负无穷)
- 当52位小数部分f不全为0时,表示NaN(Not a Number)
规格化
规格下,浮点数的表示形式如下
从上面的公式分析
s
为0或1,0表示正数,1表示负数,对应符号位f
为52位有效位,其中的每一位b
是0或1,尾数部分c
是超出52位的部分(如果有的话,需要舍入(精度会丢失))e
为十进制数值,对应11位指数部分表达的10进制数值- 1023为移码,移码值为2^{n-1}-1这里的n表示指数位数,对于64bit的双精度存储,n是11
规格化中要注意两个很重要的知识点:
- 二进制的第一位有效数字必定是1,因此这个1不会被存储,可以节省一个存储位,因此尾数部分可以存储的范围是1 ~ 2^(52+1),当然这就引入了一个新的问题,0无法合理表示了,0的二进制中没有有效数字1,所以规格化中无法表示0
- 我们知道移码主要是为了表示正负数(-1022~1023)。那么为什么要用1023移码(也就是说指数位总有一个01111111111偏移量),为什么不直接使用符号位呢?原因在于浮点数运算,我们对两个用[科学记数法]表示的数进行加减法的时候,怎么做最简单?移动小数点,让他们一致,然后把数值相加即可。计算机的处理也这样,通常所说的:求阶差,对阶,尾数相加,结果规格化。现在就面临一个问题,比较两个阶大小,同样正数01xx肯定比00xx大,这个很好理解;那么正负数呢01xx和10xx,按照之前逻辑比较就会出错,想要纠正我们要花更大的代价来处理,这是很不划算的,不如加个偏移量都变成正数吧,这样比较起来就容易多了
为了加深规格化数字印象,我们来看一个例子,计算双精度浮点数23.3
- 23转为2进制是10111
- 0.3转为2进制是010011001100(1100...)
那么实际上23.3的二进制就是10111.010011001100(1100...)改成科学计数法就是1.0111010011001100(1100...)*2^4
代入上面的例子我们知道
- 符号位:正数0
- 指数部分:e-1023 = 4 e=1027,二进制表示10000000011
- 尾数部分:忽略有效位1,并且按截取规则取得52位后得到(53位是1且之后不全是0,符合规则3)0111010011001100110011001100110011001100110011001101
截取规则
1、第53位是0,无需处理
2、第53位是1且53位之后全是0:
- 若第52位是0,无需处理;
- 若第52位是1,那么向上舍入
3、第53位是1,且之后不全是0:那么向上舍入
最终得到的64位浮点数表示就是0 10000000011 0111010011001100110011001100110011001100110011001101
非规格化
前面说了0无法规格化表示了,所以就提了出来,注意这里的指数为0,小数点前面的也是0,当f = 0(52位小数全为0)时,表示的值是0:s = 0 表示+0,s = 1表示-0
特殊值
当e = 2047(11位指数全为1)时:
- 若尾数部分 > 0 表示
NaN
- 若尾数部分 = 0, 符号位 = 0,表示
+Infinity
- 若尾数部分 = 0, 符号位 = 1,表示
-Infinity
数值范围
前面的基础都讲完了,再来看看数值范围就比较简单了
规格化
1、当指数e最大(前10位为1,11位为0,即2046)且小数f最大(52位全为1)时,能表示出最大正值
,转为十进制值为1.7976931348623157e+308
,也就是说符号位为1的时候表示的最小负值为-1.7976931348623157e+308
2、当指数e最小(前10位为0,11位为1,即1)且小数f最小(52位全为0)时,能表示出最小正值
,转为十进制值为2.2250738585072014e-308
非规格化
1、当小数f最大(52位全为1)时,能表示出最大正值
,转为十进制值为2.225073858507201e-308
,则最小负值
为-2.225073858507201e-308
2、当小数f最小(前51位为0,52位为1)时,能表示出最小正值
,转为十进制值为5e-324
,则最大负值
为-5e-324
总结
由上述可知,最大值最小值分别是
- 规格化最大值,1.7976931348623157e+308(也就是Number.MAX_VALUE)
- 非规格化最小值,5e-324(也就是Number.MIN_VALUE)
- 事实上仍然有比Number.MAX_VALUE更大的值,只是最后精度都丢失了(比Number.MAX_VALUE大的值并不一定是Infinity),比如Number.MAX_VALUE + 10值仍然是1.7976931348623157e+308,什么时候变成Infinity呢,超过MAX_VALUE精度就好了,比如Number.MAX_VALUE + Math.pow(10, 292),292 实际上是308-小数位数(16)
安全整数范围
安全整数意味着范围内的每个值都可以由64位浮点数一对一表示出来,而超出范围可能就会有精度丢失
比如我们简单点把尾数部分52位改成2位,加上省略有效位实际上3位可以表示数值(指数位先不考虑),比如我们表示(000~111)都可以一对一表示,如果要展示8(1000)呢,1.000*2^3,指数3,尾数00(0舍弃);9(1001)呢,1.001*2^3,指数3,尾数00(1舍弃),8和9的表示方法是一样的,这就不能一对一展示了,所以就有精度丢失,不安全了,所以其实最大安全整数是7(2^3-1)
对应的64位浮点数能表示的
最大安全整数
就是2^53−1 =9007199254740991
,也就是 Number.MAX_SAFE_INTEGER最小安全负整数
为-9007199254740991
,也就是Number.MIN_SAFE_INTEGER
最后我们再来看看开头的题目 0.1 + 0.2
现在就很简单了吧
0.1
- 二进制0.00011001100110011001100110011(0011...)
- 截取换算1.1001100110011001100110011001100110011001100110011010 * 2^-4
- 64位浮点数 0 01111111011 1001100110011001100110011001100110011001100110011010
0.2
- 二进制0.0011001100110011001100110011(0011...)
- 截取换算1.1001100110011001100110011001100110011001100110011010 * 2^-3
- 64位浮点数 0 01111111100 1001100110011001100110011001100110011001100110011010
浮点数加法(按照之前讲的 求阶差,对阶,尾数相加,结果规格化)阶小的尾数右移
0 01111111100 1100110011001100110011001100110011001100110011001101
+ 0 01111111100 1001100110011001100110011001100110011001100110011010
= 0 01111111100 10110011001100110011001100110011001100110011001100111
// 产生了进位。因此,阶码需要 +1,尾数部分进行低位四舍五入处理,最终结果
0 01111111101 1011001100110011001100110011001100110011001100110100
// 转换为10进制
0.3000000000000000444089209850062616169452667236328125
因为精度问题,只取到: 0.30000000000000004