又见浮点数精度问题

 

 

今天看到一篇文章: http://younglab.blog.51cto.com/416652/241886,大概是说在使用Javascript进行下面的浮点数计算时出现了问题:
 
        obj.style.opacity =  (parseInt(obj.style.opacity *100) + 1)/100;
 
obj.style.opacity是一个浮点数,范围从0~1,初始值为0。这句代码每隔一小段时间执行一次,从而让目标由透明慢慢变为不透明(淡入效果)。
 
问题是,起初obj.style.opacity还可以按照预期的每次以0.01逐步增长,但增长到0.29时就一直保持不变了。
 
做者只是记录了这个问题,没有写出为何。读完这篇博客后个人第一感受是:
 
         这又是一个因为浮点数精度所引起的问题。
 
下面让咱们来写一个小程序重现一下这个问题:
 
double opacity = 0;
for (int i = 0; i < 100; i++) {
     opacity = ((int) (opacity * 100 + 1)) / 100.0;

     System.out.println("opacity=" + opacity);
}

 

程序是用Java写的,共执行100次循环,采用了与那篇文章中相同的计算方法。正常状况下opacity会由0逐步增大到1。
 
程序输出以下:
 

opacity=0.01
opacity=0.02
opacity=0.03
opacity=0.04
opacity=0.05
opacity=0.06
(中间省略……)
opacity=0.27
opacity=0.28
opacity=0.29
opacity=0.29
opacity=0.29
……后面一直为0.29html

 
能够发现,当opacity达到0.29后便再也不增长了。因为Java和JS使用的是相同的浮点数格式,因此采用Java和JS结果都是相同的。
 
这里有一个细节须要注意:在这段程序中,除数必须写成100.0。这是因为在Java中有整数除法和浮点数除法两种不一样的运算,若是写成100,那么被除数和除数将都是整数,Java就会按照整数除法来计算,就会致使每次计算的结果都是0(由于每次计算的结果都小于1,所以取整后就变为了0)。JS里没有这个问题,由于JS没有整数除法,全部除法都会当成浮点数除法来对待。
 

深刻分析

如今我把上面那个程序作一点修改:git

 

double opacity = 0;
for (int i = 0; i < 100; i++) {
     opacity = ((int) (opacity * 100 + 1)) / 100.0;

     System.out.println("opacity=" + new BigDecimal(opacity));
     System.out.println("opacity*100=" + new BigDecimal(opacity * 100));
     System.out.println("----------------------------");
}
 
由于Java在将浮点数转换为字符串时会作一些处理,让结果看起来更“美观”一些,但这样会让咱们没法看清楚程序运行的真实状况。
 
在这个程序中我借助BigDecimal来显示浮点数在内存中的真正的样子。BigDecimal有一个以double数字为参数的构造方法,该方法会完整拷贝此double参数在内存中的位模式,它的toString( )方法也会严格按照实际的值进行转换,而不会为了“美观”而作任何处理。所以咱们能够利用这种方法来看清一个double的“真面目”。
 
程序输出以下:
 
opacity=0.01000000000000000020816681711721685132943093776702880859375
opacity*100=1
----------------------------
opacity=0.0200000000000000004163336342344337026588618755340576171875
opacity*100=2
----------------------------
opacity=0.0299999999999999988897769753748434595763683319091796875
opacity*100=3
 
(中间省略……)
 
opacity=0.270000000000000017763568394002504646778106689453125
opacity*100=27
----------------------------
opacity=0.2800000000000000266453525910037569701671600341796875
opacity*100=28.000000000000003552713678800500929355621337890625
----------------------------
opacity=0.289999999999999980015985556747182272374629974365234375
opacity*100=28.999999999999996447286321199499070644378662109375
 
……后面一直重复相同的内容
 
能够发现,当opacity的值为0.29时,实际上在内存中的准确值是0.2899999……,因此乘以100变成28.99999……,这比29要稍微小那么一点点。但就是少了这一点点,当强制转换为整数后的结果倒是28而不是指望的29。而这正是致使这个问题的缘由所在。
 
从这个程序的运行结果中咱们还能够观察到如下几个现象:
 
1. 每一个中间结果例如0.0一、0.02……等等,都没法用double类型精确表示
 
2. 即便自己没法精确表示,但在0.28以前,opacity*100的结果却都是精确的
 
3. 在没法精确表示的数中,有些比真实值略大,而有些却比真实值略小。若是是前者,当截断小数位转成整型时获得的结果是“正确”的;但若是是后者则会获得错误的结果。例如0.28*100转成整型为28,而0.29*100转成整型不是29而是28。

如何改正 

通过前面的分析,如今咱们已经弄明白了问题产生的缘由,那么该如何修正它呢?
 
以前的代码之因此没法正确运行,其根本缘由在于一个double类型的数字强制转换为整型时会发生截断,这会致使小数部分所有丢失,然而计算的中间结果中有一些要比指望的整数值略小,截断小数位之后获得的是比指望值小1的值。
 
所以咱们能够从如下两个方面着手修正此问题:一是从代码中去除强制转换操做;或者,保证截断以前的中间结果必定是比指望值略大的。
 

方法1. 去除强制转换

程序的目的是让opacity的值每次增长0.01,那么就只须要每次加上0.01就行了,彻底不须要绕圈子。以下:程序员

 

double opacity = 0;
while (opacity < 1) {
     opacity += 0.01;
     System.out.println("opacity=" + opacity);
}
 
这个程序简单、直接,并且没有任何问题。我我的推荐这个方法。该程序输出以下:
 

opacity=0.01
opacity=0.02
opacity=0.03
opacity=0.04
opacity=0.05
opacity=0.060000000000000005

(中间省略……)

opacity=0.9800000000000006
opacity=0.9900000000000007
opacity=1.0000000000000007github

 

方法2. 保证截断以前的中间结果略大于指望值

既然原程序的问题发生在截断时,那么只要保证截断发生以前,中间结果的值略大于指望值,就能保证程序的正确性。例如若是要让截断后的结果为29,只要保证截断前的值在[29, 30)这个范围内便可。
 
如何作到这一点呢?
 
因为咱们能够确定在这个问题中,opacity*100的结果是很是接近咱们所指望的整数的,只是因为double类型的精度限制而比指望的整数略大或略小而已,其偏差必定很是很是小。
 
因此咱们能够修改这句代码:
 
        opacity = ((int) (opacity * 100 + 1)) / 100.0;
 
不是给opacity * 100加上1,而是加一个更大一些的数,例如1.5,变为:
 
        opacity = ((int) (opacity * 100 + 1.5)) / 100.0;
 
若是咱们指望的值是29,那么修改后的中间结果必定是在29.5附近,这样就能保证截断后的值必定是29了。程序以下:
 
double opacity = 0;
for (int i = 0; i < 100; i++) {
     opacity = ((int) (opacity * 100 + 1.5)) / 100.0;

     System.out.println("opacity=" + opacity);
}

 

输出为:
 

opacity=0.01
opacity=0.02
opacity=0.03
opacity=0.04
opacity=0.05
opacity=0.06

(中间省略……)

opacity=0.96
opacity=0.97
opacity=0.98
opacity=0.99
opacity=1.0小程序

 
能够看到结果是正确的。
 

总结

只要稍有经验的程序员都知道浮点数不能直接进行相等比较,可是像这篇文章中所碰到的问题可能并不那么常见,所以有时不容易意识到发生了问题。
 
每一个程序员都应该知道计算机中是采用近似值来保存浮点数的,当进行浮点数相关的计算时,须要时刻提防因为精度问题所致使的偏差,并注意避免那些会影响到结果正确性的偏差(所谓正确性,就是偏差超出了所容许的最大范围)。
 
 
 

附:

 
下面这个网页列举了历史上的一些因为计算问题引发的软件灾难,其中一例是1996年欧洲航天局的Ariane 5火箭发射失败事件,该火箭发射后仅40秒即发生爆炸,致使发射基地的2名法国士兵死亡,并致使历时近10年、耗资达70亿美圆的航天计划严重受挫。过后调查报告显示问题的缘由出在火箭的惯性参考系的软件系统中,其中有一个地方是将水平方位的64位浮点数转换为一个16位的整数,当浮点数的值超过32767时,转换就会失败(即转换的结果是错误的),从而致使了悲剧的发生。
 
相关文章
相关标签/搜索