double值失精之后的表现
- Posted on:
- By: PerterPon
- With:
之前在这篇文章中解释了0.1+0.2
为什么不等于0.3
而等于0.30000000000000004
的原因,根源问题还在于存储精度的问题,当时只是简单觉得丢失精度后的行为是理所当然的,并没有丢失精度后是如何处理的,下面就来分析一下:
精度丢失加法
首先,JS,或者说double类型能精确表达的最大的数是Math.pow(2, 53)
,那么下面表达式的值你肯定也知道了
1 | Math.pow(2, 53) + 1 === 9007199254740992 |
那么下面表达式的值呢:
1 | Math.pow(2, 53) + 3 ? |
可能一开始你会想当然得认为上面的值是:9007199254740994
,因为精度不到,所以就把这位丢弃,然而,确切的结果是:
1 | 9007199254740996 |
精度丢失算法
显然上面的运算都是在精度丢失的场景下进行的,那么,如何保证精度尽量正确就是必须要做的事,
0舍1进
舍0进1类似4舍5入,0就舍弃,1就向前进一位,举个例子
Math.pow(2, 53) - 1
在内存中存储方式为:
1 | 0|10000110011|1111111111111111111111111111111111111111111111111111 |
之前我们说到有这么一个公式(注意,这里是二进制运算,那个2的N次方可以理解为十进制的10的N次方):
带入上述计算公式:
1 | 符号位 s = 0 |
所以最终结果就是M
的小数点往右移动52位的结果。
Math.pow(2, 53)
在内存中存储方式为:
1 | 0|10000110100|0000000000000000000000000000000000000000000000000000 |
1 | 符号位 s = 0 |
最终结果为M
往右移动53位
Math.pow(2, 53) + 1
在内存中存储方式为:
1 | 0|10000110100|0000000000000000000000000000000000000000000000000000 |
可以看到和Math.pow(2, 53)
一致
Math.pow(2, 53) + 2
在内存中存储方式为:
1 | 0|10000110100|0000000000000000000000000000000000000000000000000001 |
可以看到最后一位多了一个1
1 | 符号位 s = 0 |
最终结果也就是M
往右移动53位,变成了10000000000000000000000000000000000000000000000000001|0
因为右边只有52位,而我们的指数位有53位,那么最后一位就变成0了,也就是竖线后面的那一位,所以这也是精度丢失的根本原因。
Math.pow(2, 53) + 3
在内存中存储方式为:
1 | 0|10000110100|0000000000000000000000000000000000000000000000000010 |
1 | 符号位 s = 0 |
最终结果:10000000000000000000000000000000000000000000000000010|0
结论
Math.pow(2, 53)的整数位:0000000000000000000000000000000000000000000000000000|0
后面那个|
符号我们可以认为是小数点,来理解0舍1进
[a]当
Math.pow(2, 53) + 1
时,变成..000|1
,1的前值为0,舍弃[b]当
Math.pow(2, 53) + 2
时,变成..001|0
,舍弃尾部,值精确[c]当
Math.pow(2, 53) + 3
时,变成..001|1
,1的前值为1,舍0进1,变成,..010|0
- [c]当
Math.pow(2, 53) + 4
时,变成..010|0
,舍弃尾部,值精确 [c]当
Math.pow(2, 53) + 5
时,变成..010|1
,1的前值为0,舍弃[d]当
Math.pow(2, 53) + 6
时,变成..011|0
,舍弃尾部,值精确[e]当
Math.pow(2, 53) + 7
时,变成..011|1
,1的前值为1,舍0进1,变成,..100|0
- [e]当
Math.pow(2, 53) + 8
时,变成..100|0
,舍弃尾部,值精确 [e]当
Math.pow(2, 53) + 9
时,变成..100|1
,1的前值为0,舍弃[f]当
Math.pow(2, 53) + 10
时,变成..101|0
,舍弃尾部,值精确[g]当
Math.pow(2, 53) + 11
时,变成..101|1
,1的前值为1,舍0进1,变成,..110|0
- [g]当
Math.pow(2, 53) + 12
时,变成..110|0
,舍弃尾部,值精确 [g]当
Math.pow(2, 53) + 13
时,变成..110|1
,1的前值为0,舍弃[h]当
Math.pow(2, 53) + 14
时,变成..111|0
,舍弃尾部,值精确[h]当
Math.pow(2, 53) + 15
时,变成..111|1
,1的前值为1,舍0进1,变成,.1000|0
- [h]当
Math.pow(2, 53) + 16
时,变成..1000|0
,舍弃尾部,值精确 [h]当
Math.pow(2, 53) + 17
时,变成..1000|1
,1的前值为0,舍弃[i]当
Math.pow(2, 53) + 18
时,变成..1001|0
,舍弃尾部,值精确[j]当
Math.pow(2, 53) + 19
时,变成..1001|1
,1的前值为1,舍0进1,变成,.1010|0
- [j]当
Math.pow(2, 53) + 20
时,变成..1010|0
,舍弃尾部,值精确
好了,列举了20个,你大概也看出规律了,也看明白了,就不多列了。
恒1
恒1法总得来说就是在舍去的尾部总是置位1,计算方法比较简单,可以自行上网搜索。