又見浮點數精度問題

 

 

今天看到一篇文章: 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時,轉換就會失敗(即轉換的結果是錯誤的),從而致使了悲劇的發生。
 
相關文章
相關標籤/搜索