1.01 + 2.01 = 3.02
2.01 * 2.01 = 4 0401
不知你注意沒有,這個很尋常的等式,你如果將它放在C++中,Java中,Basic中,它
居然是不成立的。計算機(jī)在開玩笑嗎?噢,對了,隱約記得這好象是浮點數(shù)的問題,似乎
很多很多年前,老師說過。還有某位姓林的先生在某本書里提過=0的判斷。
嗯,如果你不遇到此問題,那你完全可以把它拋到火星上去,可惜,偶不好彩,這樣的
問題,被俺遇到了。唉!
why?how?
沒辦法,硬著頭皮,從頭開始。
一:為何不成立?Why?
這得從浮點數(shù)的在計算機(jī)內(nèi)的存儲開始說起,我這里閑話少說。我們只談雙精度double
數(shù)(至于float,基本上是五十步和一百步的區(qū)別)。
雙精度數(shù)在計算機(jī)內(nèi)的表示方式是:(三部分組成)
符號(正或負(fù)) 階碼(2的N次冪) 尾數(shù)(大于等于1小于2的數(shù))
比如: -(符號) 1.01(尾數(shù)) * 2~1(N = 1) = - 2.02
具體到計算機(jī)的存儲單元:雙精度數(shù)共占8字節(jié)(64bit)
符號位(占1個bit) 階碼(11個bit) 尾數(shù)(52個bit)
解釋一下:
符號位:0表示正 1表示負(fù)
階碼:是一個偏移量,1023的偏移量,它的1023相當(dāng)于0,小于1023時為負(fù),
大于1023時為正,如:10000000001表示指數(shù)為1025 - 1023 = 2,表示真值為2^2。
好了,知道了原理,我們開始分析上述等式為何為不等。
(相應(yīng)數(shù)的存儲值,可以簡單用C語言的指針方式取出)
1.01 表示為:
0 0111111 1111 0000 00101000 11110101 11000010 10001111 01011100 00101001
2.01 表示為;
0 1000000 0000 0000 00010100 01111010 11100001 01000111 10101110 00010100
3.02 表示為:
0 1000000 0000 1000 00101000 11110101 11000010 10001111 01011100 00101001
2.01+1.01 在編程語言中的計算結(jié)果 表示為:
0 1000000 0000 1000 00101000 11110101 11000010 10001111 01011100 00101000
好了,我們可以比較一下3.02和計算結(jié)果,果然有所不同,只不過最后一個bit不同嘿。
為了驗證一下,可以用手工計算一下2.01+1.01:
先把1.01的冪次變?yōu)?(與2.01的階碼相同),于是,將尾數(shù)右移一位。得到:
1000 00010100 01111010 11100001 01000111 10101110 000101001
加上2.01的尾數(shù)。
0000 00010100 01111010 11100001 01000111 10101110 00010100
得到:
1000 00101000 11110101 11000010 10001111 01011100 00101000
嗯,與計算機(jī)的計算結(jié)果相同,我們的運算思路是正確的。
因此,結(jié)論出來了,因為浮點數(shù)在計算機(jī)內(nèi)的存儲存在偏差,導(dǎo)致運算時,與實際期望的結(jié)
果不同。很多時候,你可以不理它,但是,可以肯定負(fù)責(zé)任的說,發(fā)射衛(wèi)星的運算時,你
需要知道,否則,衛(wèi)星一轉(zhuǎn)眼就不見了。
二:不成立的的原因找到了,那怎么解決這個問題呢,How?
一個簡單的解決辦法是:
不要用浮點數(shù)來存儲浮點,對于VC,Java,Basic,最好的辦法是用Decimal來保存它。
下面是分別的實現(xiàn):(以加法為例,其它四則運算處理相同)
VC中:
double doublAdd(double dbl1, double dbl2)
{
double dblResult;
DECIMAL dec1,dec2,decResult;
::VarDecFromR8(dbl1,&dec1);
::VarDecFromR8(dbl2,&dec2);
::VarDecAdd(&dec1,&dec2,&decResult);
::VarR8FromDec(&decResult,&dblResult);
return dblResult;
}
VB中:
Private Function doubleAdd(ByVal dbl1 As Double, ByVal dbl2 As Double) As Double
doubleAdd = CDec(dbl1) + CDec(dbl2)
End Function
Java中:
public static double add(double v1, double v2) {
BigDecimal b1 = new BigDecimal(Double.toString(v1));
BigDecimal b2 = new BigDecimal(Double.toString(v2));
return b1.add(b2).doubleValue();
}
解決思路就是:用其它精確的表示法來存儲浮點數(shù),就這么簡單。
注意:VC示例中,VarDecFromR8是做了手腳地,如果能直接用VarDecFromStr那更好。
三:在C/C++中,似乎很不情愿看到類似上例中的代碼,因為它看起來很低效,還有其它方法嗎?
好象還有,對了,只是好象。
我們再來看看雙精度數(shù)的表示法:
尾數(shù)一共有52個bit,也就是最小能表示的數(shù)是 2^-52,取對數(shù)可得出,約是
在小數(shù)點后16位,那也就是說小數(shù)點后15位是可以精確表示的,加上前置的默認(rèn)1,一共有16位
數(shù)字是精確可靠的。
我們來試驗一下,看上述結(jié)論是否成立。
看看VC調(diào)試器的顯示值。
2.01 的顯示值: 2.0099999999999998
如果只取16位有效數(shù)字,那么將最后一位8四舍五入,我們得到正確的表示。
好了,這能說明什么呢?
四:我們先看比較簡單的加,減法運算。
對于加法:dbl1 + dbl2:
假設(shè)dbl1=1.01 那么,16減去整數(shù)位1,我們可以假定,在計算機(jī)表示中:
小數(shù)點后的15位都是精確的。
假設(shè)dbl2=100.01 那么 16-3,假定小數(shù)點后13位是精確的。
憑經(jīng)驗我們可以知道,兩個小數(shù)相加,小數(shù)點后的精度不會大于精度銷大的一個。
所以,我們判定得出結(jié)果的精確度可以用較大的一個為準(zhǔn)。
于是,將得出的結(jié)果,去掉不精確的位數(shù),則應(yīng)該可以得到準(zhǔn)確值。
VC實現(xiàn)如下:
#define DELTA_RATE 16
int getRound(double dbl)
{
COleVariant var(dbl);
COleVariant varForLog(dbl);
::VarRound(&varForLog,0,&varForLog);
int nIntCount = log10(varForLog.dblVal>0?varForLog.dblVal:-varForLog.dblVal) + 1;
int nRound = DELTA_RATE - nIntCount;
return nRound;
}
double doublAdd2(double dbl1, double dbl2)
{
COleVariant var(dbl1+dbl2);
int r1 = getRound(dbl1);
int r2 = getRound(dbl2);
::VarRound(&var,max(r1,r2),&var);
return var.dblVal;
}
做過一些實驗,好象是正確的。同理可以實現(xiàn)doubleSub2的函數(shù)。
注意:這里并不用下面五所提的取精度的方式,因為取精度的運算更低效。
五:對于乘除法呢?問題有些復(fù)雜,先找出一個需要處理的例子。
如:2.01*2.01=4.0401。
試了一下,不成立。
用方法一的Decimal方式測試,可以通過。
那么方法二呢?
再做假設(shè)吧,假設(shè)dbl1有兩位小數(shù),dbl2也有兩位小數(shù),按理論,
可得出相乘后,最大可能是2+2位小數(shù)。那么,我們按照 4位小數(shù)
進(jìn)行Round處理,可能會得出正確的結(jié)果。
實際上,要取一個雙精度的10進(jìn)制表達(dá)的小數(shù)位,我沒有找到什么好辦法,
我能想到的:也就是將數(shù)字轉(zhuǎn)為字串,然后查找.后的位數(shù)。這樣,顯然是
非常低效的,這里,我就不再寫出代碼了。
六:比較方法一和方法二。方法二并不高效,并且還有一些不定因素,所以,
最好采用方法一來統(tǒng)一處理浮點數(shù)的運算。
至于效率,實際上最佳方法是從程序的設(shè)計著手,將double從程序中去除掉。
比如在VC中,可以用Variant::Decimal來徹底替換double,這樣,就不存在
中間的轉(zhuǎn)換了,效率自然就提高了。有關(guān)Decimal的常用函數(shù)是:
VarDecFromStr VarDecAdd VarDecSub VarDecMul VarDecDiv ……
VarBstrFromDec
至于Java和VB,也可以方便的找到相應(yīng)函數(shù)。
很想找到一種更好的方法,總覺得用Decimal來進(jìn)行運算很不爽,但真的沒找到?
其實呢,做了一下測試,Decimal的運算并不慢,如果可以將內(nèi)部存儲改為Decimal,
那就可以徹底解決問題了。