如何做到精確的計算 (原始碼下載)
先來看看一個 VB 計算誤差的實例:
Private Sub Command1_Click() Dim a(2) As Double, b(2) As Double Dim c(2) As Single, d(2) As Single, i As Long a(0) = 1.3 b(0) = 1 a(1) = 10.3 b(1) = 10 a(2) = 100.3 b(2) = 100 For i = 0 To 2 c(i) = a(i) d(i) = b(i) Debug.Print a(i) - b(i) Debug.Print c(i) - d(i) Next End Sub執行後在即時運算視窗可得以下結果:
0.3 0.3 0.300000000000001 0.3000002 0.299999999999997 0.3000031正確答案應該是 0.3,很明顯的 VB 算錯了,當然這不是 VB 的錯,因為電腦皆是以二進位數值來計算,所以電腦會先把十進位數值轉換成二進位;一個有限小數的十進位數值,轉換成二進位後可能變成無限小數 (更遑論無限小數的十進位數值了),但是電腦必須以有限位數來儲存這個無限小數,例如 Single 資料型態以 24bits 來儲存小數位,而 Double 資料型態以 52bits 來儲存小數位。由於 Double 儲存了比較多的小數位,因此計算結果會比 Single 準確(參考以上執行結果);另外,愈大的數值做運算,其計算誤差也愈大(參考以上執行結果),因為數值愈大,整數部分佔用的 bits 愈多,相對的小數部分就取的不足(這也是為何在做矩陣運算時,常常要用 maximization of pivot elements 的技巧來降低計算誤差的原因),請參考以下轉換成二進位的結果:
關於轉換的函數,請參考本站的 十進位與二進位的數值轉換。
十進位 二進位(Single) 1.3 1.01001100110011001100110 1 1 10.3 1010.01001100110011001100 10 1010 100.3 1100100.01001100110011001 100 1100100 以上只是示範數值相減後產生的計算誤差,其它運算也會有誤差,而且計算次數愈多,誤差將會一直累積而變大,有時會大到令人無法想像。在本站的 Least-Squares curve fitting 就示範了將變數宣告成 Single 和 Double 的誤差比較,因為計算次數非常之多,宣告成 Single 的結果和正確答案差距非常大。不過有一種情況是一定不會有計算誤差的,就是當十進位有限小數轉換成二進位時,其結果仍是有限小數,請參考以下程式:
Private Sub Command2_Click() Dim a(2) As Double, b(2) As Double Dim c(2) As Single, d(2) As Single, i As Long a(0) = 1.5 b(0) = 1 a(1) = 10.5 b(1) = 10 a(2) = 100.5 b(2) = 100 For i = 0 To 2 c(i) = a(i) d(i) = b(i) Debug.Print a(i) - b(i) Debug.Print c(i) - d(i) Next End Sub執行後在即時運算視窗可得以下結果:
0.5 0.5 0.5 0.5 0.5 0.5以下是十進位轉換成二進位的結果,很明顯的都是有限小數。
十進位 二進位(Single) 1.5 1.1 1 1 10.5 1010.1 10 1010 100.5 1100100.1 100 1100100 關於轉換的函數,請參考本站的 十進位與二進位的數值轉換。
曾經有個網友問過我,10.21-10 為何不等於 0.21,而且用 Double 算反而不比 Single 算的準確,請參考以下程式:
Private Sub Command3_Click() Dim a As Double, b As Double Dim c As Single, d As Single a = 10.21 b = 10 c = a d = b Debug.Print a - b Debug.Print c - d End Sub執行後在即時運算視窗可得以下結果:
0.210000000000001 0.21照理說 Double 應該比 Single 計算準確才對,可是這裡 Double 反而算不出正確答案,為什麼呢?老實說我也不知道答案,我將上述數值轉成二進位,並用二進位減法,最後再用 Dec 函數轉成十進位所得結果如下:
十進位 10.21 10 二進位(Single) 1010.00110101110000101000 1010 相減並轉成十進位(Single) 0.2099991 二進位(Double) 1010.001101011100001010001111010111000010100011110101 1010 相減並轉成十進位(Double) 0.209999999999997 很明顯的計算結果和 VB 不同,可能是因為 Dec 函數使用 Double 資料型態來計算而產生額外的誤差所致,關於 Dec 函數請參考本站的 十進位與二進位的數值轉換。不過這題應該是個例外情況,基本上 Double 一定是比 Single 計算準確,至於 VB 編譯器為何會這樣,如果有誰知道,請告訴我吧。謝謝!我在想或許 VB 算出 Single 的結果是 0.20999996 或 0.21000001,但是 Single 只能顯示 7 位小數,因此四捨五入後導致結果為 0.21,如此單純以 0.20999996 或 0.21000001 和 0.210000000000001 比較,還是後者比較準確。
Oops!說了半天還沒說到主題,那要如何保證計算結果真正準確呢?一般來說有兩種途徑,但是這兩種方法都必須付出佔用較大的記憶空間和運算速度較慢的代價,其一是將變數宣告成 Variant,在計算過程中再用 CDec 轉換成 Decimal 型態;其二是將變數宣告成 Currency,但是 Currency 最多只能有四位小數,請參考以下的程式:
Private Sub Command4_Click() Dim a As Variant, b As Variant Dim c As Currency, d As Currency a = 10.21 b = 10 c = a d = b Debug.Print CDec(a) - CDec(b) Debug.Print c - d End Sub執行後在即時運算視窗可得以下結果:
0.21 0.21有一點要注意的是 CDec 的使用方式,CDec 必須放在單獨的一個數值或變數內,若將 CDec 放在一個運算式內,ex. CDec(a-b),則得到的仍然是不準確的結果,因為 a-b 已經不準確,再用 CDec 轉換也為時已晚了。
06/06/1999 補充:
在這提供一段 Fortran 90 的程式碼供大家測試,由於類似 Fortran 這種數值導向的程式語言,為了求運算快速,編譯器在建立 object program 時是採用切斷法(chopping),而不是捨入法(rounding),因此計算誤差反而比 VB 來的大,請參考以下程式:
program test implicit none real(kind=4)::a1,b1 real(kind=8)::a2,b2 a1=10.3 b1=10.0 a2=10.3 b2=10.0 write(*,*) a2-b2 write(*,*) a1-b1 stop end program test您可以使用一個免費的 Fortran 90 編譯器來編譯上述程式碼,例如 Essential Lahey Fortran 90 ,執行結果如下:
0.300000190734863 0.300000誤差是不是比 VB 來的大呢!而其單精度的計算結果看起來沒有誤差,其實是因為 Fortran 的單精度有效位數為 6 位,因此誤差是被 Fortran truncate 掉了。由於我使用的是 Lahey 免費編譯器,如果您有其它 Fortran 編譯器,也可以幫忙測試看看,若是得到更好的計算結果,請告訴我,謝謝囉!
在此提供各位降低計算誤差的三種方法:
使用更精確的變數來計算:例如 Decimal 或 Currency資料型態,但是一般來說使用 Double 就夠了。
使用更好的演算法:例如在做矩陣運算時,常常用 maximization of pivot elements 的技巧來降低捨入誤差,請參考本站的這個程式。
減少計算次數:例如 x3+3x2+3x+1 將需要 5 次乘法運算,3 次加法運算;若改寫成巢狀式(nested),(x+1)3 將只需要 2 次乘法運算,1 次加法運算,不但降低計算誤差,同時也增加執行效率喔!
This page was written by Jaric on May. 28, 1999. All rights reserved.
Revised : Jun. 6, 1999