浮點(diǎn)型在內(nèi)存中的存儲(chǔ)分布方式因機(jī)器平臺(tái)而異,完全理解所有機(jī)器平臺(tái)中的浮點(diǎn)型存儲(chǔ)無疑是一件相當(dāng)麻煩的事。幸運(yùn)的是,大多機(jī)器平臺(tái)都遵守 IEEE-754 標(biāo)準(zhǔn),很可能讀者和我使用的平臺(tái)正是使用的 IEEE-754 標(biāo)準(zhǔn)。
在C語言程序開發(fā)中,數(shù)值的處理是一門值得深究的科學(xué)。本文不可能將復(fù)雜的數(shù)值算法以及相關(guān)的C語言程序開發(fā)經(jīng)驗(yàn)一一列出。事實(shí)上,討論如何以理想的數(shù)值精度進(jìn)行計(jì)算,就和討論如何編寫最快的C語言程序,如何設(shè)計(jì)一款優(yōu)秀的軟件一樣,主要取決于程序員本身的綜合素質(zhì)。
鑒于此,這里將嘗試介紹一些基礎(chǔ)的,我認(rèn)為每個(gè)C語言程序員都應(yīng)該知道的內(nèi)容。首先,我們應(yīng)該明白C語言程序開發(fā)中的兩個(gè)浮點(diǎn)數(shù)何時(shí)相等。可能讀者并不覺得難,因?yàn)樗坪魿語言中的 == 運(yùn)算符就能判斷兩個(gè)浮點(diǎn)數(shù)是否完全相等。然而實(shí)際上,C語言中的 == 運(yùn)算符是逐位比較兩個(gè)操作數(shù)的,而兩個(gè)浮點(diǎn)數(shù)的精度總是有限的,在這種場景下,== 運(yùn)算符的實(shí)際使用意義就沒有那么大了。
讀者應(yīng)該已經(jīng)明白,計(jì)算機(jī)存儲(chǔ)浮點(diǎn)數(shù)時(shí),很有可能是需要舍棄一些位的(如果該浮點(diǎn)數(shù)過長),如果 CPU 或者相應(yīng)的程序沒有按照預(yù)期四舍五入,那么使用 == 運(yùn)算符判斷兩個(gè)浮點(diǎn)數(shù)是否相等可能會(huì)失敗。例如,標(biāo)準(zhǔn)C語言函數(shù)庫三角函數(shù) cos() 的實(shí)現(xiàn)其實(shí)只是一種多項(xiàng)式近似,也就是說,我們并不能指望 cos(π/2) 結(jié)果的每一個(gè)位都為零。在C語言程序開發(fā)中,我們甚至不能準(zhǔn)確的表示 π。
假設(shè)在某段C語言程序中有兩個(gè)數(shù)字 1.25e-20 和 2.25e-20,它倆的差值是 1e-20,遠(yuǎn)小于 EPSILON,但是顯然它倆并不相等。但是如果這兩個(gè)數(shù)字是 1.2500000e-20和1.2500001e-20,那么就可以認(rèn)為它們是相等的。也就是說,兩個(gè)數(shù)字距離足夠接近時(shí),我們還需要關(guān)注需要匹配多少有效數(shù)字。
計(jì)算機(jī)存儲(chǔ)空間總是有限的,因此數(shù)值溢出是C語言程序員最關(guān)心的問題之一。讀者應(yīng)該已經(jīng)知道,如果向C語言中的最大無符號整數(shù)加一,該整數(shù)將歸零,令人崩潰的是,我們并不能只通過看這個(gè)數(shù)字的方式獲知是否有溢出發(fā)生,歸零的整數(shù)看起來和標(biāo)準(zhǔn)零一模一樣。當(dāng)溢出發(fā)生時(shí),實(shí)際上大多數(shù) CPU 是會(huì)設(shè)置一個(gè)標(biāo)志位的,如果讀者懂得匯編,可以通過檢查該標(biāo)志位獲知是否有溢出發(fā)生。
float 浮點(diǎn)數(shù)溢出時(shí),我們可以方便的使用 +/- inf(無窮)。+inf(正無窮)大于任何數(shù)字,-inf(負(fù)無窮)小于任何數(shù)字,inf+1 等于 inf ,依此類推。因此在C語言程序開發(fā)中,一個(gè)小技巧是,將整數(shù)轉(zhuǎn)換為浮點(diǎn)數(shù),這樣就方便判斷后續(xù)處理是否會(huì)造成溢出了。處理完畢后,再將該數(shù)轉(zhuǎn)換回整數(shù)即可。
不過,將整數(shù)轉(zhuǎn)換為浮點(diǎn)數(shù)判斷是否溢出也是要付出代價(jià)的,因?yàn)楦↑c(diǎn)數(shù)可能沒有足夠的精度來保存整個(gè)整數(shù)。32 位的整數(shù)可以表示任何 9 位十進(jìn)制數(shù),但是 32 位的浮點(diǎn)數(shù)最多只能表示 7 位的十進(jìn)制數(shù)。所以,如果將一個(gè)很大的整數(shù)轉(zhuǎn)換為浮點(diǎn)數(shù),可能不會(huì)得到期望的結(jié)果。此外,在C語言程序開發(fā)中,int 與 float 之間的數(shù)值類型轉(zhuǎn)換,包括 float 與 double 之間的數(shù)值類型轉(zhuǎn)換,實(shí)際上是會(huì)帶來一定的性能開銷的。
讀者應(yīng)該明白,在C語言程序開發(fā)中,不管是否使用整數(shù),都應(yīng)該小心避免數(shù)值溢出的發(fā)生,不僅僅是最開始和最終結(jié)果數(shù)值可能溢出,在一些計(jì)算的中間過程,可能會(huì)產(chǎn)生一些更大的值。一個(gè)經(jīng)典的例子是“C語言數(shù)字配方”計(jì)算復(fù)數(shù)的幅度問題,極可能造成數(shù)值溢出的C語言實(shí)現(xiàn)是下面這樣的:
假設(shè)該復(fù)數(shù)的實(shí)部 re 和虛部 im 都等于 1e200,那么它們的幅度約為 1.414e200,這的確在雙精度的允許范圍內(nèi)。但是,上述C語言代碼的中間過程將產(chǎn)生 1e200 的平方值,也即 1e400,這超出了 inf 的范圍,此時(shí)上面的實(shí)現(xiàn)函數(shù)計(jì)算的平方根將仍然是無窮大。
應(yīng)該明白,上述C語言代碼為了避免數(shù)值溢出,給出的實(shí)現(xiàn)實(shí)際上是一種近似。例如 im 等于 1e200,re 等于 1,那么 im/re 的平方仍然能夠達(dá)到 1e400,這會(huì)造成數(shù)值溢出。但是平方 re/im 卻是可以的,因?yàn)?1e-400 會(huì)被四舍五入到零,足夠接近得到正確的答案。
幸運(yùn)的是,就像上面求復(fù)數(shù)幅度避免出現(xiàn)數(shù)值溢出一樣,避免兩個(gè)接近的數(shù)字相減出現(xiàn)精度損失的方法也是有的,但是并沒有一個(gè)通用的方法。這里給出一個(gè)簡單的實(shí)例就是使用 1/x 的函數(shù)代替 x 的函數(shù),這對于處理二次運(yùn)算很有效。我的建議是,如果讀者發(fā)現(xiàn)自己的C語言程序給出了令人懷疑的數(shù)值,就應(yīng)該檢查一下相應(yīng)的減法運(yùn)算了。
看到這里,相信讀者應(yīng)該想到C語言程序中的加法可能也有同樣的問題:假設(shè)有數(shù)字 1.0,現(xiàn)在將其與 1e-20 相加。程序很可能認(rèn)為 1e-20 很小,小于 EPSILON,于是忽略它,得到答案 1.0。這實(shí)際上也是一種精度損失。要完全規(guī)避C語言程序中的浮點(diǎn)數(shù)可能帶來的問題,工作量無疑是巨大的。為了簡化問題,我們通常認(rèn)為浮點(diǎn)數(shù)帶來的精度問題是這樣的:對浮點(diǎn)數(shù)的操作越多,損失的精度也會(huì)越多。
正如前文舉的例子 cos(π/2),C語言它的實(shí)現(xiàn)實(shí)際上是一種近似,得到的答案并不是 0,而是 6.12303E-17。不過就這個(gè)答案本身而言,它已經(jīng)足夠小,認(rèn)為等于 0 也沒什么大問題。但是如果我們下一步計(jì)算是除以 1e-17,那么得到的答案就約是 6 了,這與預(yù)期的零相差甚遠(yuǎn)。
最后,在C語言程序開發(fā)中,并不是只有浮點(diǎn)數(shù)才重要的,整數(shù)同樣重要,它的精確性是一個(gè)有用的工具。有時(shí)程序需要跟蹤變化的分?jǐn)?shù)(例如比例因子)。在這種情況下,既然浮點(diǎn)數(shù)受各種因素影響,那么我們完全可以將該分?jǐn)?shù)存儲(chǔ)為整數(shù)分子和分母來避免問題。在需要使用浮點(diǎn)數(shù)時(shí),隨時(shí)再做一次除法運(yùn)算就可以了。
評論
查看更多