浮點數如何存儲與表示(精度問題)


計算機中的數值表示

為特定數據選擇其在計算機中的存儲與表示方式時,主要考慮以下幾個因素

  1. 要表示的數的類型(小數、整數、實數或復數);
  2. 可能需要的數值范圍
  3. 數值的精確度要求;
  4. 數據存儲和處理所需要的硬件代價

定點數與浮點數

整數在計算機中一般使用整型處理,其在內存中的存儲形式為二進制補碼,有關整型數據的存儲與表示詳見機器碼與位運算這篇文章。下面重點講一講實數中的另一塊兒——小數

小數是實數的一種特殊的表現形式,小數中的圓點叫做小數點,它是一個小數的整數部分小數部分的分界號。其中整數部分是零的小數叫做純小數,整數部分不是零的小數叫做帶小數。

小數點在計算機中通常有兩種表示方法,一種是約定所有數據的小數點隱含在某一個固定位置上,稱為定點表示法,簡稱定點數;另一種是小數點的位置可以浮動,稱為浮點表示法,簡稱浮點數

定點數表示法(fixed-point number)

所謂定點格式,即約定機器中所有數據的小數點位置是固定不變的。在計算機中通常采用兩種簡單的約定:將小數點的位置固定在數據的最高位之前,或者是固定在最低位之后。一般常稱前者為定點小數,后者為定點整數

定點小數

定點小數是純小數,約定的小數點位置在符號位之后、有效數值部分最高位之前。若數據x的形式為$x = x_0.x_1x_2...x_n$(其中$x_0$為符號位,$x_1~x_n$是數值的有效部分,也稱為尾數,$x_1$為最高有效位),則在計算機中的表示形式為:

一般說來,如果最末位$x_n = 1$,前面各位都為$0$,則數的絕對值最小,即$|x|_{min} = 2^{-n}$。如果各位均為$1$,則數的絕對值最大,即$|x|_{max} = 1-2^{-n}$。所以定點小數的表示范圍是:$$2^{-n} \le |x| \le 1-2^{-n}$$

定點整數

定點整數是純整數,約定的小數點位置在有效數值部分最低位之后。若數據x的形式為$x = x_0x_1x_2...x_n.$(其中$x_0$為符號位,$x_1~x_n$為尾數,$x_n$為最低有效位 ),則在計算機中的表示形式為:

定點整數的表示范圍是:$$1 \le |x| \le 2^n-1$$

溢出

當數據的絕對值小於定點數能表示的最小值時,計算機將它們作$0$處理,稱為下溢;大於定點數能表示的最大值時,計算機將無法表示,稱為上溢,上溢和下溢統稱為溢出。

運算機制與缺陷

計算機采用定點數表示時,對於既有整數又有小數的原始數據,需要設定一個比例因子,數據按其縮小成定點小數或擴大成定點整數再參加運算,運算結果,根據比例因子,還原成實際數值。比例因子選擇不當,往往會使運算結果產生溢出降低數據的有效精度

用定點數進行運算處理的計算機被稱為定點機。        

浮點數表示法(floating-point number)

定點數表達法的缺點在於其形式過於僵硬,固定的小數點位置決定了固定位數的整數部分和小數部分,不利於同時表達特別大的數或者特別小的數。所以,絕大多數現代的計算機系統采納了所謂的浮點數表示法

原理

利用科學計數法來表達實數,即任意一個$J$進制數$N$,總可以寫成$$N = J^E \times M$$

式中$M$稱為數$N$的尾數(mantissa),是一個純小數;$E$為數$N$的階碼(exponent),是一個整數,$J$稱為比例因子$J^E$的底數。這種表示方法相當於數的小數點位置隨比例因子的不同而在一定范圍內可以自由浮動,所以稱為浮點表示法。

比如$123.45$用十進制科學計數法可以表達為$1.2345 × 10^2$,其中$1.2345$為尾數,$10$為底數,$2$為指數。浮點數利用指數達到了浮動小數點的效果,從而可以靈活地表達更大范圍的實數。提示: 尾數有時也稱為有效數字(Significand),尾數實際上是有效數字的非正式說法。

底數是事先約定好的(常取2),在計算機中不出現。在機器中表示一個浮點數時,

一是要給出尾數,用定點小數形式表示。尾數部分給出有效數字的位數,因而決定了浮點數的表示精度

二是要給出階碼,用整數形式表示,階碼指明小數點在數據中的位置,因而決定了浮點數的表示范圍

三是浮點數也有符號位

因此一個機器浮點數應當由階碼、尾數及其符號位組成:

其中$E_S$表示階碼的符號,占一位,$E_1~E_n$為階碼值,占$n$位,尾符是數$N$的符號,也要占一位。當底數取$2$時,二進制數$N$的小數點每右移一位,階碼減小$1$,相應尾數右移一位;反之,小數點每左移一位,階碼加$1$,相應尾數左移一位。

規范浮點數

若不對浮點數的表示作出明確規定,同一個浮點數的表示就不是唯一。例如比如上面例子中的$123.45$可以表達為$12.345 × 10^1$,$0.12345 × 10^3$或者 $1.2345 × 10^2$。因為這種多樣性,有必要對其加以規范化以達到統一表達的目標。規范的(Normalized)浮點數表達方式具有如下形式:

$$d.dd...d \times \beta^{e}, (0 \le d_i \le \beta)$$

其中$d.dd...d$即尾數,$\beta$為底數,$e$為指數。尾數中數字的個數稱為精度,在本文中用$p$(precision)來表示。每個數字$d$介於0和底數$\beta$之間,包括$0$,小數點左側的數字不為$0$。

基於規范表達的浮點數對應的具體值可由下面的表達式計算而得:$$\pm(d_0 + d_ 1\beta^{-1} + ... + d_{p-1}\beta^{-(p-1)}\beta^e), (0 \le d_i < \beta)$$

二進制浮點數

對於十進制的浮點數,即底數$\beta$等於$10$的浮點數而言,上面的表達式非常容易理解,也很直白。而計算機內部的數值表達是基於二進制的,上面的表達式同樣適用於二進制。

二進制的浮點數,其科學記數法的形式為:$$\pm X_nX_{n-1}...X_0.X_{-1}X_{-2}...X_{-m} = \pm X_{n}.X_{n-1}...X_0X_{-1}X_{-2}...X_{-m} \times 2^n$$

其中$X_i$只能是0或1。規范化表示為:$$\pm X_nX_{n-1}...X_0.X_{-1}X_{-2}...X_{-m} = \pm 1.X_{n-1}...X_0X_{-1}X_{-2}...X_{-m} \times 2^n$$

即約定小數點位於最高位的1之后,因此$X_n$不能是0。既然整數位只能是1,那么這一位可以不用存儲,稱之為隱含位,也就是說大家心里明白就行。

這樣做的好處是可以多存儲一位小數部分,但是在作浮點運算時需要特殊處理,運算之前要補齊這一位,運算之后又得略去這一位,會有一點點性能損耗;如果有效位本來就足夠多,省去整數位也賺不了多少便宜,這可能是擴展雙精度浮點數不采用這種方案的原因

例如:二進制數$1001.101$相當於:$$1 \times 2^3 + 0 \times 2^2 + 0 \times 2^1 + 1 \times 2^0 + 1 \times 2^{-1} + 0 \times 2^{-2} + 1 \times 2^{-3}$$

對應於十進制的$9.6254$,其規范浮點數表達為$1.001101 \times 2^3$。

IEEE (美國電氣和電子工程師學會)754標准

計算機中是用有限連續字節保存浮點數的。

IEEE定義了多種浮點格式,但最常見的是三種類型:單精度、雙精度和擴展雙精度,分別適用於不同的計算要求。一般而言,單精度適合一般計算,雙精度適合科學計算,擴展雙精度適合高精度計算。一個遵循IEEE 754標准的系統必須支持單精度類型(強制類型),最好也支持雙精度類型(推薦類型),至於擴展雙精度類型可以隨意。

IEEE 754 標准定義的單精度浮點數存儲格式為32位,雙精度浮點數存儲格式為64位。此外,它還定義了兩種擴展格式,即float-extended-exponent和double-extended-exponent擴展格式。后兩種這里不做介紹,感興趣的讀者可自行查找資料學習。

浮點數的存儲規范

在 IEEE 標准中,浮點數是將特定長度的連續字節的所有二進制位分割為特定寬度的符號域指數域尾數域三個域,其中保存的值分別用於表示給定二進制浮點數中的符號,指數和尾數。這樣,通過尾數和可以調節的指數(所以稱為“浮點“”)就可以表達給定的數值了。

下表列出C++中不同精度浮點數內存布局:

需要特別注意的是,擴展雙精度類型沒有隱含位,因此它的有效位數與尾數位數一致,而單精度類型和雙精度類型均有一個隱含位,因此它的有效位數比尾數位數多一個

表示方法

IEEE 754標准規定一個實數$V$可以用:$V = (-1)^S \times 2^{E - bias} \times (1.M)$  V=(-1)s×M×2^E的形式表示,以單精度浮點數為例,則有:

其中

S:符號位

0表示數值為正數,而1則表示負數,對數值$0$的符號位特殊處理

E:指數域

對應於二進制科學計數法中的指數部分,通常使用移碼表示(移碼和補碼只有符號位相反,其余都一樣)。

其中單精度數為8位,雙精度數為11以單精度數為例,8位的指數為可以表達0~255之間的256個指數值。

指數偏移

但是,指數可以為正數,也可以為負數。為了處理負指數的情況,實際的指數值加上了一個偏移(Bias)值作為保存在指數域中的值。單精度浮點數的bias為127(0-111 1111)(8位),而雙精度bias為1023(0-11 1111 1111)(11位)。

實際存儲值 = 真值 + 對應浮點數類型的偏移值

N位指數的編碼和真值有下列關系:$$E = e + 2^{N-1} - 1$$

其中,$E$是指數實際存儲值,$e$是真值,float類型的指數編碼就是:$E = e + 127$,[1, 127)是負數,[127, 254]是正數,0和255有特殊用途。

邊界指數值

<1> 實際指數值為-127保存為全0),則有:-127原碼1-111 1111 => 補碼1-000 0001 => 加上單精度偏移值: 0-111 111(127) => 結果:0-000 0000(全0)。所以0-000 0000指數位表示:-127,即$e^{-127}$。

<2> 實際指數值為+128保存為全1),則有:+128原碼1-000 0000 => 補碼1-000 0000 => 加上單精度偏移值:0-111 111(127) => 結果:1-111 1111(全1)。所以1-111 1111指數位表示:+128,即$e^{+128}$。

這些特殊的指數值,保留用作特殊浮點數的處理。(特殊浮點數將在下文重點介紹)

M:尾數域

M也稱為有效數字,二進制小數,取值范圍為$0 \le M < 1$。

其中單精度浮點數為23位長,雙精度為52位長。除了我們將要講到的某些特殊浮點數外,IEEE 標准要求浮點數必須是規范的。這意味着尾數的小數點左側必須為1,因此我們在保存尾數的時候,可以省略小數點前面這個1,從而騰出一個二進制位來保存更多的尾數。這樣我們實際上用23位長的尾數域表達了24位的尾數。比如對於單精度數而言,二進制的$1001.101$(對應於十進制的$9.625$)可以表達為$1.001101 × 2^3$,所以實際保存在尾數域中的值為$001 1010 0000 0000 0000 0000$,即去掉小數點左側的1,並用0在右側補齊。

示例

以float為例:$$\pm 1.f \times 2^{E-127}$$

比如十進制數$123.125$,其二進制表示為:$1111011.001$,規格化表示為:$1.111011001 \times 2^6$也就是$1.111011001 \times 2^{133-127}$,$f=111011001$,$E=133$,二進制為$10000101$,圖示如下

 

因為尾數域實際上可以精確表示24位尾數,所以這里可以得出一個結論:任意一個int值(二進制表示),只要存在這樣的序列:從最低位開始找到第一個1,然后從這個1向高位數移動24位停下,如果更高的位上不再有1,那么該int值即可被float精確表示,否則就不行。簡單說,就是第一個1開始到最后一個1為止的總位數超過24,那么該int值就不能被float類型精確表示,例:

圖中能被丟棄的0,在尾數上體現出來,丟棄一個0,尾數的1就前移1位,並沒有損失精度。

很容易得出,從1開始的連續整數里面第一個不能被float精確表示的整數,其二進制形式為:1 0000 0000 0000 0000 0000 0001,即16777217:$1.000000000000000000000001 \times 2^{24}$,$f$有24位,最后一個1只能舍棄,也就是$1.00000000000000000000000 \times 2^{24}$,即$1.0 \times 2^{24}$,這個數實際上是16777216。也就是說16777217和16777216的內存表示是一樣的:

那么16777217之后的下一個可以被float精確表示的int值是多少呢?很簡單,向16777217上不斷的加1,直到滿足“第一個1開始到最后一個1為止的總位數為24位”:

$1 0000 0000 0000 0000 0000 0010$就是16777218,規范化表示為:$1.00000000000000000000001 \times 2^{24} = 1.00000000000000000000001 \times 2^{151-127}$,其$f$是23位(最后一位是1)。

特殊浮點數

IEEE標准定義了6類浮點數:

有限數就是遵循規范的常規浮點數,其指數在最小值(-127)和最大值(+128)之間(兩邊均不包含),且整數位恆為1,其形式為:$$\pm1.f \times 2^{e-bias}$$

例如float,其指數E有8位,取值范圍為(0, 255)也就是[1, 254]。有限數在運算過程中最常見的問題就是溢出,即運算結果無法用有限數表示。

以下約定$e_{min} = -126$,$e_{max} = +127$。

有符號的零

$$\pm(0.0) \times 2^{0-bias}$$

因為IEEE標准的浮點數格式中,小數點左側的$1$是隱藏的,而零顯然需要尾數必須全是零。所以,零也就無法直接用這種格式表達而只能特殊處理。實際上,零保存為尾數域為全為$0$,指數域為$e_{min} - 1 = -127$,也就是說指數域也全為$0$。考慮到符號域的作用,所以存在着兩個零,即$+0$和$-0$。不同於正負無窮之間是有序的,IEEE 標准規定正負零是相等的

零為什么要分正負?

零有正負之分,的確非常容易讓人困惑。這一點是基於數值分析的多種考慮,經利弊權衡后形成的結果。

有符號的零可以避免運算中,特別是涉及無窮的運算中,符號信息的丟失

舉例而言,如果零無符號,則等式$1/(1/x) = x$當$x = \pm \infty$時不再成立。原因是如果零無符號,$1$和正負無窮的比值為同一個零,然后$1$與$0$的比值為正無窮,符號沒有了。解決這個問題,除非無窮也沒有符號。但是無窮的符號表達了上溢發生在數軸的哪一側,這個信息顯然是不能不要的。

為什么正負零相等?

零有符號也造成了其它問題,比如當$x = y$時,等式$1/x = 1/y$在$x$和$y$分別為$+0$和$-0$時,兩端分別為正無窮和負無窮而不再成立。當然,解決這個問題的另一個思路是和無窮一樣,規定零也是有序的。但是,如果零是有序的,則即使 if (x == 0) 這樣簡單的判斷也由於$x$可能是$±0$而變得不確定了。兩害取其輕者,零還是無序的(正0應該和負0相等,而不應正0大於負0)。

弱規范數

我們來考察浮點數的一個特殊情況。

選擇兩個絕對值極小的浮點數,以單精度的二進制浮點數為例,比如$1.001 \times 2^{-125}$和$1.0001 \times 2^{-125}$這兩個數(分別對應於十進制的$2.6448623 \times 10^{-38}$和$2.4979255 \times 10^{-38}$)。顯然,他們都是普通的浮點數(指數為-125,大於允許的最小值-126,尾數更沒問題),按照 IEEE 754可以分別保存為$0000 0001 0001 0000 0000 0000 0000 0000(0x1100000)$和$0000 0001 0000 1000 0000 0000 0000 0000(0x1080000)$。

現在我們看看這兩個浮點數的差值。不難得出,該差值為$0.0001 \times 2^{-125}$,表達為規范浮點數則為$1.0 \times 2^{-129}$。問題在於其指數大於允許的最小指數值,所以無法保存為規范浮點數最終,只能近似為零(Flush to Zero)。這中特殊情況意味着下面本來十分可靠的代碼也可能出現問題:

if (x != y) {
    z = 1 / (x -y);
}

正如我們精心選擇的兩個浮點數展現的問題一樣,即使x不等於y,x和y的差值仍然可能絕對值過小,而近似為零,導致除以0的情況發生。

為了解決此類問題,IEEE標准中引入了非規范(Denormalized)浮點數。規定當浮點數的指數為允許的最小指數值($e_{min}$ )時,尾數不必是規范化的。比如上面例子中的差值可以表達為非規范的浮點數$0.001 \times 2^{-126}$,其中指數-126等於$e_{min}$。注意,這里規定的是“不必”,這也就意味着“可以”。

指數域特殊處理(無隱含位)

當浮點數實際的指數為$e_{min}$,且指數域也為$e_{min}$時,該浮點數仍是規范的,也就是說,保存時隱含着一個隱藏的尾數位。為了保存非規范浮點數,IEEE標准采用了類似處理特殊值零時所采用的辦法,即用特殊的指數域值$e_{min} - 1$加以標記,當然,此時的尾數域不能為零。這樣,例子中的差值可以保存為$0000 0000 0001 0000 0000 0000 0000 0000(0x100000),沒有隱含的尾數位

弱規范數的指數域和零一樣,都是全0,但沒有隱含位,尾數部分不為0,其形式為:$$\pm(f) \times 2^{0-bias}$$

去掉了隱含的尾數位的制約,可以保存絕對值更小的浮點數。也由此,上述關於極小差值的問題也不存在了,因為所有可以保存的浮點數之間的差值同樣可以保存了

弱規范數 <=> 有限數

弱規范數的整數位是尾數的最高位(沒有隱含位),在向有限數轉換時,要向高位移一位,以產生隱含位,但指數不變(不減1);從有限數形式轉換成弱規范數形式時,正好相反,向低位移1位,指數仍不變。

弱規范數的意義

在計算過程中,如果中間結果小於最小的有限數卻不是0,即出現下溢,如果當做0處理可能會導致計算終止引入“弱規范數”之后,在0和最小的有限數之間有相當一部分數可以表示為“弱規范數”,從而提高了計算能力

無窮

特殊值無窮(Infinity)$\infty$的指數部分為全1(即最大值),整數位是1(即隱含位),尾數是0,其形式為:$$\pm(1.0) \times 2^{MAX-bias}$$

float類型即:$\pm(1.0) \times 2^{255-bias}$,產生$\infty$的情形一般有:

  • 自身運算,例如$\infty + 1.0 = \infty$
  • 被0除,例如$1/+0 = +\infty, 1/-0 = -\infty$
  • 上溢,及計算結果超出了類型范圍

無窮用於表達計算中產生的上溢(Overflow)問題

比如兩個極大的數相乘時,盡管兩個操作數本身可以保存為浮點數,但其結果可能大到無法保存為浮點數,而必須進行舍入。根據 IEEE 標准,此時不是將結果舍入為可以保存的最大的浮點數(因為這個數可能離實際的結果相差太遠而毫無意義),而是將其舍入為無窮。對於負數結果也是如此,只不過此時舍入為負無窮,也就是說符號域為1的無窮。特殊值無窮使得計算中發生的上溢錯誤不必以終止運算為結果

無窮除NaN及零以外的其它浮點數一樣是有序的,從小到大依次為:負無窮 => 負的有窮非零值 => 正負零 => 正的有窮非零值 => 正無窮。

除NaN以外的任何非零值除以零,結果都將是無窮,而符號則由作為除數的零的符號決定。  

NaN(Not a Number)

NaN,即Not a Number,和$\infty$一樣,指數部分為全1(即最大值),整數位是1(即隱含位),但尾數部分不為0,其形式為:$$\pm(1.f) \times 2^{MAX-bias}$$其中,$f$不為0。

NaN用於處理計算中出現的錯誤情況,比如0.0除以0.0或者求負數的平方根

NaN有SNaN(Signal NaN)和QNaN(Quiet NaN)之分,IEEE標准要求:SNaN參與運算要觸發異常,而QNaN則不觸發異常。兩者的區別在於,SNaN的尾數最高位是0,而QNaN的尾數最高位是1。

IEEE標准只規定NaN的尾數不為0,沒有要求具體的尾數域,所以NaN實際上不是一個,而是一族。這就給予了具體實現一定的空間:不同的實現可以自由選擇尾數域的值來表達NaN,當計算出現問題時,在尾數部分設置的這些特殊值將有利於調試。

實際上,所有的NaN值都是無序的。NaN有一些晦澀的運算規則

  • $0 \times \infty$ = NaN,因此“0乘任何數都是0”不是恆成立的。 
  • 邏輯運算關系運算符 <, <=, >, >= 和 == 在任一操作數為NaN時均返回false。即使是兩個具有相同位模式的NaN,== 也返回false。而操作符 != 則當任一操作數為NaN時返回true(這個規則的一個有趣結果是 x != x 當x為NaN時竟然為真!)。因此在浮點數的邏輯運算中,編譯器沒有辦法做積極的優化,因為如果有NaN參與邏輯運算,比如x = NaN,那么 !(x < y) 和 x >= y 就不等價了。
  • 零除以零,結果不是無窮而是NaN。原因:當除數和被除數都逼近於零時,其商可能為任何值,所以IEEE標准決定此時用NaN作為商比較合適。
  • 此外,任何有NaN作為操作數的操作也將產生NaN。

用特殊的NaN來表達上述運算錯誤的意義在於避免了因這些錯誤而導致運算的不必要終止例如,如果一個被循環調用的浮點運算方法,可能由於輸入的參數問題而導致發生這些錯誤,NaN使得即使某次循環發生了這樣的錯誤,也可以簡單地繼續執行循環以進行那些沒有錯誤的運算。你可能想到,既然Java有異常處理機制,也許可以通過捕獲並忽略異常達到相同的效果。但是,要知道,IEEE標准不是僅僅為Java而制定的,各種語言處理異常的機制不盡相同,這將使得代碼的遷移變得更加困難。何況,不是所有語言都有類似的異常或者信號(Signal)處理機制。

編譯器不一定遵循IEEE標准的規定,可想而知,浮點運算有多難搞。】

范圍和精度

表示范圍:浮點數 > 定點數

浮點數所表示的范圍比定點數大。假設機器中的數由8位二進制數表示(包括符號位):在定點機中這8位全部用來表示有效數字(包括符號);在浮點機中若階符、階碼占3位,尾符、尾數占5位,在此情況下,若只考慮正數值,定點機小數表示的數的范圍是0.0000000到0.1111111,相當於十進制數的$0$到$\frac{127}{128}$,而浮點機所能表示的數的范圍則是$2^{-11} \times 0.0001$到$2^{11} \times 0.1111$,相當於十進制數的$\frac{1}{128}$到$7.5$ 。顯然,都用8位,浮點機能表示的數的范圍比定點機大得多。

盡管浮點表示能擴大數據的表示范圍,但浮點機在運算過程中,仍會出現溢出(Flow)現象。一般稱大於最大絕對值的數據為上溢(Overflow),小於最小絕對值的數據為下溢(Underflow)。

下面以階碼占3位,尾數占5位(各包括1位符號位)為例,來討論這個問題。下圖給出了相應的規范化浮點數的數值表示范圍。

“可表示的負數區域”和“可表示的正數區域”及“0”,是機器可表示的數據區域上溢區是數據絕對值太大,機器無法表示的區域;下溢區是數據絕對值太小,機器無法表示的區域。若運算結果落在上溢區,就產生了溢出錯誤,使得結果不能被正確表示,要停止機器運行,進行溢出處理。若運算結果落在下溢區,也不能正確表示之,機器當0處理稱為機器零

一般來說,增加尾數的位數,將增加可表示區域數據點的密度,從而提高了數據的精度增加階碼的位數,能增大可表示的數據區域

最大最小值

最小的正float有限數

根據有限數的規范化形式,指數取最小值1,隱含位是1,尾數取最小值0(23位都是0):

$$1.0 \times 2^{1-127} = 1.0 \times 2^{-126} = 1.1754943508222875079687365372222e^{-38}$$

 如果浮點運算的結果小於這個數,就出現下溢,一般將其結果轉換為最小的有限數,或者弱規范數,如果弱規范數也不能表示,那么將轉換成0

最大的float有限數

根據有限數的規格化形式,指數取最大值254,隱含位是1,尾數取最大值(23位都是1):

$$1.11111111111111111111111111 \times 2^{254-127} = (2-2^{-23}) \times 2^{127}$$

$$= 2^{128} - 2^{104} = 3.4028234663852885981170418348452e^{38}$$

如果浮點運算的結果超過這個數,就出現上溢,一般將其結果轉換為最近的有限數或$\infty$

最小的正float弱規范數

根據弱規范數的規格化形式,尾數取最小值:

$$0.0000000000000000000001 \times 2^{0-127} = 2^{-22} \times 2^{-127}$$

$$= 2^{-149} = 1.4012984643248170709237295832899e^{-45}$$

$f$ 是23位,且最高位是整數位,因此小數點之后只有22位,最后一位為1,即可得出該數。

FLT_EPSILON

FLT_EPSILON是C++定義的一個float數,該數是滿足$1.0 + FLT_EPSILON ! = 1.0$的最小的float有限數,比該數還小的float有限數會有:$1.0 + x = 1.0$。根據這個定義,從比1.0大的最小float有限數開始推導:

$$1.00000000000000000000001 \times 2^{127-127} = 1.00000000000000000000001$$

$$= > FLT\_EPSILON = 0.00000000000000000000001 = 2^{-23}$$

$$= 0.00000011920928955078125 \approx 1.192092896e^{-7}$$

它的規范化表示:$1.0 \times 2^{104-127}$,$E = 104$,$f = 0$ 。注意,該數遠不是最小的正float有限數,它比最小的正float有限數還要“大很多”,它的指數是-23,而最小的正float有限數的指數是-126

這個數並沒有什么特別的意義,在代碼中無條件的使用$fabs(x-y)$ < FLT_EPSILON判斷兩個浮點數是否相等,可能會導致問題。如果你的系統中浮點數很小,極端來說,甚至小於FLT_EPSILON判斷兩個浮點數是否相等,可能會導致問題。如果你的系統中浮點數很小,極端來說,甚至小於FLT_EPSILON,那你用$fabs(x-y)$ < FLT_EPSILON來判斷相等,顯然是錯誤的,你應該自己定義一個可以接受的誤差范圍來輔助判斷相等問題。

精度與誤差

很多小數根本無法在二進制計算機中精確表示(比如最簡單的0.1)。

因為浮點數尾數域的位數有限,為此,浮點數的處理辦法是持續該過程直到由此得到的尾數足以填滿尾數域,之后對多余的位進行舍入

換句話說,除了我們之前講到的精度問題之外,十進制到二進制的變換也並不能保證總是精確的,而只能是近似值。事實上,只有很少一部分十進制小數具有精確的二進制浮點數表達。再加上浮點數運算過程中的誤差累積,結果是很多我們看來非常簡單的十進制運算在計算機上卻往往出人意料。這就是最常見的浮點運算的“不准確”問題。

存儲格式的范圍和精度如下表所示:

舍入

值得注意的是,對於單精度數,由於我們只有24位的尾數(其中一位隱藏),所以可以表達的最大指數為$2^{24} - 1 = 16777215$。

特別的,16777216是偶數,所以我們可以通過將它除以2並相應地調整指數來保存這個數,這樣16777216同樣可以被精確保存。相反的,數值16777217則無法被精確保存。由此,我們可以看到單精度浮點數可以表達的十進制數值中,真正有效的數字不高於8位。

事實上,對相對誤差的數值分析結果顯示有效的精度大約為7.22位。

實例如下所示:

根據標准要求,無法精確保存的值必須向最接近的可保存的值進行舍入。這有點像我們熟悉的十進制的四舍五入,即不足一半則舍,一半以上(包括一半)則進

不過,對於二進制浮點數而言,還多一條規矩,就是當需要舍入的值剛好是一半時,不是簡單地進,而是在前后兩個等距接近的可保存的值中,取其中最后一位有效數字為零者。從上面的示例中可以看出,奇數都被舍入為偶數,且有舍有進。我們可以將這種舍入誤差理解為“半位”的誤差。所以,為了避免7.22對很多人造成的困惑,有些文章經常以7.5位來說明單精度浮點數的精度問題。

注意:這里采用的浮點數舍入規則有時被稱為舍入到偶數(Round to Even)。相比簡單地逢一半則進的舍入規則,舍入到偶數有助於從某些角度減小計算中產生的舍入誤差累積問題,因此為 IEEE 標准所采用

結語

浮點運算,深奧、晦澀、難懂!我們對浮點運算的所有想當然的假設可能都是不靠譜的。正如Herb Sutter所說,世界上的人可以分3種:
  ● 一種是知道自己不懂浮點運算(我就是);
  ● 一種是以為自己懂浮點運算;
  ● 最后一種是極少的專家級人物,他們想知道自己是否有可能最終完全理解浮點運算。

 (整理自網絡)

參考資料:

https://blog.csdn.net/qq_36396104/article/details/80045050

https://blog.csdn.net/whyel/article/details/81067989


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM