JS浮点数运算时的bug问题分析与解决方法
問題分析
- 0.2+0.4=0.600 000 000 000 000 1
0.58*10=5.8
,但0.58*100=57.999 999 999 999 99
,0.58*1000=580
这里讨论的都是二进制格式的浮点数表示,不包括十进制等其他进制的表示。
浮点数表示的误差
整数的表示
但是如果有效数字太多,则可能会出问题。比如对于12 345 678 901 234 567 000,即使使用了double(binary64)来表示,结果为1.010 101 101 010 100 101 010 011 000 110 011 101 011 000 111 110 000 1×263,但这个二进制表示其实代表的却是1.234 567 890 123 456 7e19。
小数0.6如何表示?
对于整数,二进制表示只会丢失有效数字,而不会有其他的编号。然而对于小数,则可能会很麻烦,因为小数的二进制表示可能是无限循环的。
比如,对于0.6,的binary64表示:
(0.6)10 = (0.100 110 011 001 100 110 011 001 100 110 011 001 100 110 011 001 100 110 011 001 100 1...)2
= (1.001 100 110 011 001 100 110 011 001 100 110 011 001 100 110 011 001 100 110 011 001...)2×2-1
于是,符号位s=0,指数e为-1,有效数字为1.001 100 110 011 001 100 110 011 001 100 110 011 001 100 110 011 001 100 110 011 001...
我们知道,binary64的有效数字最多有53位,也就是说
1.001 100 110 011 001 100 110 011 001 100 110 011 001 100 110 011 001 100 110 011 001... 的黄色部分需要被抛弃。
那么我们应该如何抛弃这部分数据呢?在IEEE中规定了若干舍入方法,一般来说,普遍使用的是roundTiesToEven方式来舍入。
roundTiesToEven方法:round到相邻的浮点数据上。如果两个浮点数据都一样近, 则round到最后一位是偶数的浮点数据上。
由此,0.6~=(-1)0×1.001 100 110 011 001 100 110 011 001 100 110 011 001 100 110 011 001 1×2-1。
再试试0.2和0.4
(0.2)10 =(0.001 100 110 011 001 100 110 011 001 100 110 011 001 100 110 011 001 100 110 011 001 1...)2
=(1.100 110 011 001 100 110 011 001 100 110 011 001 100 110 011 001 100 110 011 001 1...)2×2-3 。
(0.4)10 =(0.011 001 100 110 011 001 100 110 011 001 100 110 011 001 100 110 011 001 100 110 011 0...)2
=(1.100 110 011 001 100 110 011 001 100 110 011 001 100 110 011 001 100 110 011 001 10...)2×2-2。
使用roundTiesToEven方式舍入黄色部分后,前面的部分加1。也即
0.2 =(1.100 110 011 001 100 110 011 001 100 110 011 001 100 110 011 001 101 0)2×2-3
0.4 =(1.100 110 011 001 100 110 011 001 100 110 011 001 100 110 011 001 101 0)2×2-2
其实,0.2和0.4的有效位数是一样的,只是指数不同。
到现在,我们可以发现。0.2和0.4都是向上取整的,也即浮点数表示的值比实际值是要大那么一丢丢的。
浮点数计算误差
当采用-3作为指数时,0.2和0.4需要表示成
0.2 =( 1.100 110 011 001 100 110 011 001 100 110 011 001 100 110 011 001 101 0)2×2-3
0.4 =(11.001 100 110 011 001 100 110 011 001 100 110 011 001 100 110 011 010 )2×2-3
可以看到,小数位后,0.4少了一位,我们需要用0补齐,然后计算加法。得到
0.2+0.4=(100.110 011 001 100 110 011 001 100 110 011 001 100 110 011 001 100 111 0×2-3)2
规则化
0.2+0.4=(1.001 100 110 011 001 100 110 011 001 100 110 011 001 100 110 011 001 110)2×2-1
我们又需要舍弃黄色部分了,在这里,由于它离两边是一样近的,于是我们round到最后一位是偶数的浮点数上,于是有
0.2+0.4=(1.001 100 110 011 001 100 110 011 001 100 110 011 001 100 110 011 010 0__)2×2-1
请注意:这里我们又往上取整了,也即此处的结果是比实际值要大那么一丢丢的。
而这个结果到底是多少呢?精确结果是0.600 000 000 000 000 088 817 841 970 012 523 233 890 533 447 265 625。
考虑到有效数字,小数点后只保留16位,所以需要舍弃黄色部分,其结果是0.600 000 000 000 000 1=0.600 000 000 000 000 0 + 1E-16。
0.2+0.4的最后那个1来自哪里?
- 对0.2舍入时,我们偏大了一丢丢。>=1.5 × 2-57
- 对0.4舍入时,我们偏大了一丢丢。>=1.5 × 2-56
- 对0.2+0.4舍入时,我们偏大了一丢丢。= 1 × 2-54
- 在输出时,我们又偏大了一丢丢。
最后的总结
- 浮点数表示可能存在Round
- 浮点数计算可能存在Round
- 结果输出时可能存在Round
解决方法
郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。