程序員必知之浮點數運算原理詳解

程序員必知之浮點數運算原理詳解

  導讀:浮點數運算是一個很是有技術含量的話題,不太容易掌握。許多程序員都不清楚使用==操做符比較float/double類型的話到底出現什麼問題。 許多人使用float/double進行貨幣計算時常常會犯錯。這篇文章是這一系列中的精華,全部的軟件開發人員都應該讀一下。程序員

  隨着你經驗的增加,你確定 想去深刻了解一些常見的東西的細節,浮點數運算就是其中之一。架構

1. 什麼是浮點數?編碼

  在計算機系統的發展過程當中,曾經提出過多種方法表達實數。spa

  【1】典型的好比相對於浮點數的定點數(Fixed Point Number)。在這種表達方式中,小數點固定的位於實數全部數字中間的某個位置。貨幣的表達就可使用這種方式,好比 99.00 或者 00.99 能夠用於表達具備四位精度(Precision),小數點後有兩位的貨幣值。因爲小數點位置固定,因此能夠直接用四位數值來表達相應的數值。SQL 中的 NUMBER 數據類型就是利用定點數來定義的。code

  【2】還有一種提議的表達方式爲有理數表達方式,即用兩個整數的比值來表達實數。orm

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

  【3】浮點數表達方式, 這種表達方式利用科學計數法來表達實數,即用一個尾數(Mantissa ),一個基數(Base),一個指數(Exponent)以及一個表示正負的符號來表達實數。好比 123.45 用十進制科學計數法能夠表達爲 1.2345 × 102 ,其中 1.2345 爲尾數,10 爲基數,2 爲指數。浮點數利用指數達到了浮動小數點的效果,從而能夠靈活地表達更大範圍的實數。提示: 尾數有時也稱爲有效數字(Significand)。尾數其實是有效數字的非正式說法。ci

  一樣的數值能夠有多種浮點數表達方式,好比上面例子中的 123.45 能夠表達爲 12.345 × 101,0.12345 × 103 或者 1.2345 × 102。由於這種多樣性,有必要對其加以規範化以達到統一表達的目標。規範的(Normalized)浮點數表達方式具備以下形式:開發

  d.dd...d × βe , (0 ≤ di < β)it

  其中 d.dd...d 即尾數,β 爲基數,e 爲指數。尾數中數字的個數稱爲精度,在本文中用 p(presion) 來表示。每一個數字 d 介於 0 和基數β之間,包括 0。小數點左側的數字不爲 0。

(1)  基於規範表達的浮點數對應的具體值可由下面的表達式計算而得:(p是精度個數)

  ±(d0 + d1β-1 + ... + dp-1β-(p-1)e , (0 ≤ di < β)

  對於十進制的浮點數,即基數 β 等於 10 的浮點數而言,上面的表達式很是容易理解,也很直白。計算機內部的數值表達是基於二進制的。從上面的表達式,咱們能夠知道,二進制數一樣能夠有小數點,也 一樣具備相似於十進制的表達方式。只是此時 β 等於 2,而每一個數字 d 只能在 0 和 1 之間取值。

(2)  好比二進制數 1001.101 至關於:精度爲7

   1 × 2 3 + 0 × 22 + 0 × 21 + 1 × 20 + 1 × 2-1 + 0 × 2-2 + 1 × 2-3,對應於十進制的 9.625。

  其規範浮點數表達爲 1.001101 × 23

(3)  IEEE (美國電氣和電子工程師學會)浮點數

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

  IEEE定義了多種浮點格式,但最多見的是三種類型:單精度、雙精度、擴展雙精度,分別適用於不一樣的計算要求。通常而言,單精度適合通常計算,雙精度適合科學計算,擴展雙精度適合高精度計算。一個遵循IEEE 754標準的系統必須支持單精度類型(強制類型)、最好也支持雙精度類型(推薦類型),至於擴展雙精度類型能夠隨意。單精度(Single Precision)浮點數是32位(即4字節)的,雙精度(Double Precision)浮點數是64位(即8字節)的。

  保存這些浮點數固然必須有特定的格式,Java 平臺上的浮點數類型 float 和 double 採納了 IEEE 754 標準中所定義的單精度 32 位浮點數和雙精度 64 位浮點數的格式。注意: Java 平臺還支持該標準定義的兩種擴展格式,即 float-extended-exponent 和 double-extended-exponent 擴展格式。這裏將不做介紹,有興趣的讀者能夠參考相應的參考資料。

  在 IEEE 標準中,浮點數是將特定長度的連續字節的全部二進制位分割爲特定寬度的符號域,指數域和尾數域三個域,其中保存的值分別用於表示給定二進制浮點數中的符號,指數和尾數。這樣,經過尾數和能夠調節的指數(因此稱爲"浮點")就能夠表達給定的數值了。

  具體的格式參見下面的表格:

  

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


   

  IEEE754標準規定一個實數V能夠用:  V=(-1)s×M×2^E的形式表示,說明以下:
  (1)符號s(sign)決定實數是正數(s=0)仍是負數(s=1),對數值0的符號位特殊處理。
  (2)有效數字M是二進制小數,M的取值範圍在1≤M<2或0≤M<1。
  (3)指數E(exponent)是2的冪,它的做用是對浮點數加權。


   爲了強制定義一些特殊值,IEEE標準經過指數將表示空間劃分紅了三大塊:

  【1】最小值指數(全部位全置0)用於定義0和弱規範數

  【2】最大指數(全部位全值1)用於定義±∞和NaN(Not a Number)

  【3】其餘指數用於表示常規的數

  這樣一來,最大(指絕對值)常規數的指數不是全1的,最小常規數的指數也不是0,而是1。

  

  S:符號位,    Exponent:指數域    Fraction:尾數域

  注意:尾數有時也稱爲有效數字(Significand),

   通常如1.001001*2EValue,即一個尾數(Mantissa ),一個基數(底數Base),一個指數Evalue表示

  即: M * BE = 尾數 * 底數指數

  一般狀況,IEEE標準寫法,尾數的1,省略,Fraction= 0.001001,由於標準寫法,前面的1老是省略 Fraction = 尾數 - 1 IEEE規定小數點左側的 1 是隱藏的

  若是指數值:加上相應的浮點數偏執後的值:即 Exponent = EValue + Bias

  因此上述的值: X = (-1)S  X ( 1 + Fraction) (Exponent - Bias), 也就不足爲奇了


 

  在上面的圖例中:

  ①  第一個域:爲符號域。其中 0 表示數值爲正數,而 1 則表示負數

  ②  第二個域爲指數域,對應於咱們以前介紹的二進制科學計數法中的指數部分。

  指數閾:一般使用移碼錶示:

  移碼和補碼只有符號位相反,其他都同樣。對於正數而言,原碼反碼補碼都同樣對於負數而言,補碼就是其絕對值的原碼所有取反,而後加1(不包括符號位))。

  其中單精度數爲 8 位,雙精度數爲 11 位。以單精度數爲例,8 位的指數爲能夠表達 0 到 255 之間的 255 個指數值

  可是,指數能夠爲正數,也能夠爲負數。爲了處理負指數的狀況,實際的指數值按要求須要加上一個誤差(Bias)值做爲保存在指數域中的值,單精度數的誤差值爲 127(0-111 1111)(8位),而雙精度數的誤差值爲 1023(0-1 1111 1111)(10位)。好比,單精度的實際指數值 0指數域中將保存爲 127;而保存在指數域中的 64 則表示實際的指數值 -63誤差的引入使得對於單精度數,實際能夠表達的指數值的範圍就變成 -127 到 128 之間(包含兩端)[-127, 128]

  咱們不久還將看到:

  實際的指數值 -127(保存爲 全 0),即: 首先-127原碼1-111 1111,的補碼1-000 0001,而後加上單精度偏執: 0-111 111 ,即結果:0-000 0000,全0.  因此0-000 0000 指數位表示:-127,即e-127

  以及 +128(保存爲全 1), 即:首先+128原碼‘1’-000 0000,的補碼, ‘1’-000 0000,而後加上單精度偏執:0-111 111 ,, 即結果:‘1’-111 1111,全1。   即全1 指數位表示:+128,即e+128

  這些特殊值,保留用做特殊值的處理。這樣,實際能夠表達的有效指數範圍就在 -127 和 127 之間。在本文中,最小指數和最大指數分別用 emin 和 emax 來表達。


 

  計算機中的符號數有三種表示方法,即原碼、反碼和補碼。

  如補碼的求取:
    ①  正數(符號位爲0的數)補碼與原碼相同.
    ②  負數(符號位爲1的數)變爲補碼時符號位不變,其他各項取反,最後在末尾+1;即求負數的反碼不包括符號位
  例如:正數  原碼01100110,補碼爲:01100110
     負數  原碼11100110,先變反碼:10011001,再加1變爲補碼:10011010
  計算機中的符號數有三種表示方法,即原碼、反碼和補碼。三種表示方法均有符號位和數值位兩部分符號位都是用0表示「正」,用1表示「負」,而數值位,三種表示方法各不相同。
  在計算機系統中,數值一概用補碼來表示和存儲。緣由在於:①使用補碼,能夠將符號位和數值域統一處理;②同時,加法和減法也能夠統一處理。此外,③補碼與原碼相互轉換,其運算過程是相同的,不須要額外的硬件電路
  特性
  ①  一個負整數(或原碼)與其補數(或補碼)相加,和爲模。eg:原碼11100110, 補碼:10011010 和:
  ②  對一個整數的補碼再求補碼,等於該整數自身。
  ③  補碼的正零負零表示方法相同。即 0-0000000, 1-0000000取反加1, 0-0000000


 

  ③  圖例中的第三個域爲尾數域,其中單精度數爲 23 位長,雙精度數爲 52 位長。除了咱們將要講到的某些特殊值外,IEEE 標準要求浮點數必須是規範的。這意味着尾數的小數點左側必須爲 1,所以咱們在保存尾數的時候,能夠省略小數點前面這個 1,從而騰出一個二進制位來保存更多的尾數。這樣咱們實際上用 23 位長的尾數域表達了 24 位的尾數。好比對於單精度數而言,二進制的 1001.101(對應於十進制的 9.625)能夠表達爲 1.001101 × 23,因此實際保存在尾數域中的值爲 00110100000000000000000,即去掉小數點左側的 1,並用 0 在右側補齊。

   根據IEEE(美國電氣和電子工程師學會)754標準要求,沒法精確保存的值必須向最接近的可保存的值進行舍入。這有點像咱們熟悉的十進制的四捨五入,即不足一半則舍,一半以上(包括一半)則進。不過對於二進制浮 點數而言,還多一條規矩,就是當須要舍入的值恰好是一半時,不是簡單地進,而是在先後兩個等距接近的可保存的值中,取其中最後一位有效數字爲零者。從上面 的示例中能夠看出,奇數都被舍入爲偶數,且有舍有進。咱們能夠將這種舍入偏差理解爲"半位"的偏差。因此,爲了不 7.22 對不少人形成的困惑,有些文章常常以 7.5 位來講明單精度浮點數的精度問題。

  據以上分析,IEEE 754標準中定義浮點數的表示範圍爲:

  單精度浮點數  二進制:± (2-2^-23) × 2127    對應十進制:  ~ ± 10^38.53

  雙精度浮點數    二進制:± (2-2^-52) × 21023

  浮點數的表示有必定的範圍,超出範圍時會產生溢出(Flow),通常稱大於絕對值最大的數據爲上溢(Overflow),小於絕對值最小的數據爲下溢(Underflow)。

 

2. 浮點數的表示約定

  單精度浮點數和雙精度浮點數都是用IEEE 754標準定義的,其中有一些特殊約定,例如:

  (1)  當P=0,M=0時,表示0。
  (2)  當P=255,M=0時,表示無窮大,用符號位來肯定是正無窮大仍是負無窮大。
  (3)  當P=255,M≠0時,表示NaN(Not a Number,不是一個數)。

 

3. 特殊值

 經過前面的介紹,你應該已經瞭解的浮點數的基本知識,這些知識對於一個不接觸浮點數應用的人應該足夠了。不過,若是你興趣正濃,或者面對着一個棘手的浮點數應用,能夠經過本節瞭解到關於浮點數的一些值得注意的特殊之處。

  咱們已經知道,單精度浮點數指數域實際能夠表達的指數值的範圍爲 -127 到 128 之間(包含兩端)。其中,值 -127(保存爲全0)以及 +128(保存爲全1)保留用做特殊值的處理。本節將詳細 IEEE 標準中所定義的這些特殊值。

  浮點數中的特殊值主要用於特殊狀況或者錯誤的處理。好比在程序對一個負數進行開平方時,一個特殊的返回值將用於標記這種錯誤,該值爲 NaN(Not a Number)。沒有這樣的特殊值,對於此類錯誤只能粗暴地終止計算。除了 NaN 以外,IEEE 標準還定義了 ±0,±∞ 以及非規範化數(Denormalized Number)。

  對於單精度浮點數,全部這些特殊值都由保留的特殊指數值 -127 和 128 來編碼。若是咱們分別用 eminemax 來表達其它常規指數值範圍的邊界,即 -126 和 127,則保留的特殊指數值能夠分別表達爲 emin - 1 和 emax + 1; 。基於這個表達方式,IEEE 標準的特殊值以下所示:

  

  其中 f 表示尾數中的小數點右側的(Fraction)部分,即標準記法中的有效部分-1

  第一行即咱們以前介紹的普通的規範化浮點數。隨後咱們將分別對餘下的特殊值加以介紹。

  第2,3,4,5行,是特殊值。

(1)NaN

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

  由上面的表中能夠看出,對於單精度浮點數,NaN 表示爲指數爲 emax + 1 = 128(指數域全爲 1),且尾數域不等於零的浮點數。IEEE 標準沒有要求具體的尾數域,因此 NaN 實際上不是一個,而是一族

  不一樣的實現能夠自由選擇尾數域的值來表達 NaN,好比 Java 中的常量 Float.NaN 的浮點數可能表達爲 0-11111111-10000000000000000000000,其中尾數域的第一位爲 1,其他均爲 0(不計隱藏的一位),但這取決系統的硬件架構。Java 中甚至容許程序員本身構造具備特定位模式的 NaN 值(經過 Float.intBitsToFloat() 方法)。好比,程序員能夠利用這種定製的 NaN 值中的特定位模式來表達某些診斷信息。定製的 NaN 值,能夠經過 Float.isNaN() 方法斷定其爲 NaN,可是它和 Float.NaN 常量卻不相等。

  實際上,全部的 NaN 值都是無序的。數值比較操做符 <,<=,> 和 >= 在任一操做數爲 NaN 時均返回 false。等於操做符 == 在任一操做數爲 NaN 時均返回 false,即便是兩個具備相同位模式的 NaN 也同樣。而操做符 != 則當任一操做數爲 NaN 時返回 true。

  這個規則的一個有趣的結果是 x!=x 當 x 爲 NaN 時居然爲真

  NaN

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

(2)無窮

  和 NaN 同樣,特殊值無窮(Infinity)的指數部分一樣爲 emax + 1 = 128,不過無窮的尾數域必須爲零。無窮用於表達計算中產生的上溢(Overflow)問題。好比兩個極大的數相乘時,儘管兩個操做數自己能夠用保存爲浮點數,但其結果可能大到沒法保存爲浮點數,而必須進行舍入。根據 IEEE 標準,此時不是將結果舍入爲能夠保存的最大的浮點數(由於這個數可能離實際的結果相差太遠而毫無心義),而是將其舍入爲無窮對於負數結果也是如此,只不過此時舍入爲負無窮,也就是說符號域爲 1 的無窮。有了 NaN 的經驗咱們不難理解,特殊值無窮使得計算中發生的上溢錯誤沒必要以終止運算爲結果。

  無窮和除 NaN 之外的其它浮點數同樣是有序的,從小到大依次爲負無窮,負的有窮非零值,正負零(隨後介紹),正的有窮非零值以及正無窮。除 NaN 之外的任何非零值除以零,結果都將是無窮,而符號則由做爲除數的零的符號決定。  

  回顧咱們對 NaN 的介紹,當零除以零時獲得的結果不是無窮而是 NaN 。緣由不難理解,當除數和被除數都逼近於零時,其商可能爲任何值,因此 IEEE 標準決定此時用 NaN 做爲商比較合適。

(3)有符號的零

  由於 IEEE 標準的浮點數格式中,小數點左側的 1 是隱藏的,而零顯然須要尾數必須是零。因此,零也就沒法直接用這種格式表達而只能特殊處理。實際上,零保存爲尾數域爲全爲 0,指數域爲 emin - 1 = -127,也就是說指數域也全爲 0。考慮到符號域的做用,因此存在着兩個零,即 +0 和 -0。不一樣於正負無窮之間是有序的,IEEE 標準規定正負零是相等的

 零有正負之分,的確很是容易讓人困惑。這一點是基於數值分析的多種考慮,經利弊權衡後造成的結果。有符號的零能夠避免運算中,特別是涉及無窮的運算中,符號信息的丟失。舉例而言,若是零無符號,則等式 1/(1/x) = x 當x = ±∞ 時再也不成立。緣由是若是零無符號,1 和正負無窮的比值爲同一個零,而後 1 與 0 的比值爲正無窮,符號沒有了。解決這個問題,除非無窮也沒有符號。可是無窮的符號表達了上溢發生在數軸的哪一側,這個信息顯然是不能不要的。零有符號也形成了其它問題,好比當 x=y 時,等式1/x = 1/y 在 x 和 y 分別爲 +0 和 -0 時,兩端分別爲正無窮和負無窮而再也不成立。固然,解決這個問題的另外一個思路是和無窮同樣,規定零也是有序的。可是,若是零是有序的,則即便 if (x==0) 這樣簡單的判斷也因爲 x 多是 ±0 而變得不肯定了。兩害取其輕者,零仍是無序的好。

(4)非規範化數

  咱們來考察浮點數的一個特殊狀況。選擇兩個絕對值極小的浮點數,以單精度的二進制浮點數爲例,好比 1.001 × 2-125 和 1.0001 × 2-125 這兩個數(分別對應於十進制的 2.6448623 × 10-38 和 2.4979255 × 10-38)。顯然,他們都是普通的浮點數(指數爲 -125,大於容許的最小值 -126;尾數更沒問題),按照 IEEE 754 能夠分別保存爲 00000001000100000000000000000000(0x1100000)和 00000001000010000000000000000000(0x1080000)。
  
如今咱們看看這兩個浮點數的差值。不可貴出,該差值爲 0.0001 × 2-125,表達爲規範浮點數則爲 1.0 × 2-129。問題在於其指數大於容許的最小指數值,因此沒法保存爲規範浮點數。最終,只能近似爲零(Flush to Zero)。這中特殊狀況意味着下面原本十分可靠的代碼也可能出現問題:

if (x != y) {

 z = 1 / (x -y);

}

  正如咱們精心選擇的兩個浮點數展示的問題同樣,即便 x 不等於 y,x 和 y 的差值仍然可能絕對值太小,而近似爲零,致使除以 0 的狀況發生。

  爲了解決此類問題,IEEE 標準中引入了非規範(Denormalized)浮點數。規定當浮點數的指數爲容許的最小指數值,即 emin 時,尾數沒必要是規範化的。好比上面例子中的差值能夠表達爲非規範的浮點數 0.001 × 2-126,其中指數 -126 等於 emin。注意,這裏規定的是"沒必要",這也就意味着"能夠"。當浮點數實際的指數爲 emin,且指數域也爲 emin 時,該浮點數還是規範的,也就是說,保存時隱含着一個隱藏的尾數位。爲了保存非規範浮點數,IEEE 標準採用了相似處理特殊值零時所採用的辦法,即用特殊的指數域值 emin - 1 加以標記,固然,此時的尾數域不能爲零。這樣,例子中的差值能夠保存爲 00000000000100000000000000000000(0x100000),沒有隱含的尾數位。
  有了非規範浮點數,去掉了隱含的尾數位的制約,能夠保存絕對值更小的浮點數。並且,也因爲再也不受到隱含尾數域的制約,上述關於極小差值的問題也不存在了,由於全部能夠保存的浮點數之間的差值一樣能夠保存。

4. 範圍和精度

 不少小數根本沒法在二進制計算機中精確表示(好比最簡單的 0.1)因爲浮點數尾數域的位數是有限的,爲此,浮點數的處理辦法是持續該過程直到由此獲得的尾數足以填滿尾數域,以後對多餘的位進行舍入。

  換句話說,除了咱們以前講到的精度問題以外,十進制到二進制的變換也並不能保證老是精確的,而只能是近似值

  事實上,只有不多一部分十進制小數具備精確的二進制浮點數表達。再加上浮點數運算過程當中的偏差累積,結果是不少咱們看來很是簡單的十進制運算在計算機上卻每每出人意料。這就是最多見的浮點運算的"不許確"問題。

  參見下面的 Java 示例:

System.out.print("34.6-34.0=" + (34.6f-34.0f));

  這段代碼的輸出結果以下:

34.6-34.0=0.5999985

  產生這個偏差的緣由是 34.6 沒法精確的表達爲相應的浮點數,而只能保存爲通過舍入的近似值這個近似值與 34.0 之間的運算天然沒法產生精確的結果。

  存儲格式的範圍和精度以下表所示:

5. 舍入

  值得注意的是,對於單精度數,因爲咱們只有 24 位的尾數(其中一位隱藏),因此能夠表達的最大指數爲 224 - 1 = 16,777,215。

  特別的,16,777,216 是偶數,因此咱們能夠經過將它除以 2 並相應地調整指數來保存這個數,這樣 16,777,216 一樣能夠被精確的保存。相反,數值      16,777,217 則沒法被精確的保存。由此,咱們能夠看到單精度的浮點數能夠表達的十進制數值中,真正有效的數字不高於 8 位

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

  實例以下所示:

  

  根 據標準要求,沒法精確保存的值必須向最接近的可保存的值進行舍入。這有點像咱們熟悉的十進制的四捨五入,即不足一半則舍,一半以上(包括一半)則進。不過 對於二進制浮點數而言,還多一條規矩,就是當須要舍入的值恰好是一半時,不是簡單地進,而是在先後兩個等距接近的可保存的值中,取其中最後一位有效數字爲 零者。從上面的示例中能夠看出,奇數都被舍入爲偶數,且有舍有進。咱們能夠將這種舍入偏差理解爲"半位"的偏差。因此,爲了不 7.22 對不少人形成的困惑,有些文章常常以 7.5 位來講明單精度浮點數的精度問題。

  提示: 這裏採用的浮點數舍入規則有時被稱爲舍入到偶數(Round to Even)。相比簡單地逢一半則進的舍入規則,舍入到偶數有助於從某些角度減少計算中產生的舍入偏差累積問題。所以爲 IEEE 標準所採用

相關文章
相關標籤/搜索