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