有趣的二進位制3—浮點數

wier發表於2017-11-29
關於浮點數很多人都知道計算機會丟失精度的問題,那麼是精度如何丟失的,為何要引入IEEE 754規範,以及非規範化浮點數有何用途?深究這些問題你會發現很難回答上來,這篇做個回顧,方便你更快的梳理這些關鍵知識點。

本篇是二進位制系列第三篇,如若你有興趣,請持續關注,後期會持續更新。其他文章列表如下:

有趣的二進位制

有趣的二進位制—高效位運算


一、 精度

如果你有看過《有趣的二進位制》這篇文章,你就會明白進位制(不侷限於二進位制)中的小數是如何表示。因為每種進位制都有其侷限性,也就是約數的問題,比如


  • 同樣是1/3,在三進位制下正好分乾淨,但在十進位制下就總也分不完。十進位制只能近似的表示1/3而無法精確的表示1/3。
  • 同樣是0.1,在十進位制下可以精確的表示為0.1,而在二進位制下只能近似的表示0.00011001100110011...(迴圈0011)


計算機中,儲存資料的方式是採用二進位制,因此在有限的儲存空間下,絕大部分的十進位制小數都不能用二進位制浮點數來精確表示。一般情況下,你輸入的十進位制浮點數僅由實際儲存在計算機中的近似的二進位制浮點數表示。不要相信浮點數結果精確到了最後一位,也永遠不要比較兩個浮點數是否相等。

需要說明的是,雖然目前計算機表示的精度多數都是近似值,但多數情況下都夠用了,如果你對精度有更高要求,每一種語言都有自己實現和處理方式。特別要注意的是防止上溢和下溢現象的發生。


二 、 IEEE 754 標準

1、IEEE 754

IEEE二進位制浮點數算術標準IEEE 754)是20世紀80年代以來最廣泛使用的浮點數運算標準,為許多CPU與浮點運算器所採用。主要規範如下:


這個公式中:

  1. s(Sign):符號,表示是正數還是負數。0正數,1負數
  2. E(Exponent):指數域,E是2的指數,類似於科學計數法中(a×10^n)中的n,如此E是負數就可以表示小數了。不過這裡E是一個無符號整數,32位的時候,E的佔用8位,取值範圍0-255,儲存E的時候我們稱為e,它是E的值與一個固定值(32位的情況是127)的和(e=E+bias,E=e-bias,bias=127)。採用這種方式表示是因為,指數的值可能為正也可能為負,如果採用補碼錶示的話,符號位S和Exp自身的符號位將導致不能簡單的進行大小比較。
  3. M(Mantissa):尾數域,浮點數具體的數值. 1≤M<2,隱含的以1開頭表示,M表示的二進位制表示式為1.xxx...,第一位總是1,因此IEEE 754規定,儲存M時,預設這個數的第一位總是1,因此可以被捨去,只儲存後面的xxxxxx部分。


2、示例

比如十進位制的3.25,我們分為以下幾步進行轉換。

1、轉換二進位制

3.25 這整數部分3轉換為二進位制是11,小數部分0.25 轉換為二進位制為2^-2,也可以按照乘以 2 取整數位的方法:

(1) 0.25 x 2 = 0.5  取整數位 0 得 0.0
(2) 0.5 x 2 = 1  取整數位 1 得 0.01複製程式碼

如此3.25轉化為二進位制為11.01


2、有效數(Mantissa)

上述規則我們知道,尾數部分

最高有效位
(即整數字)是1,也就是尾數有一位隱含的二進位制有效數字。因此我們轉換尾數的時候,始終保持最高位為1(小數點左邊),通過二進位制科學計數法轉換,我們得到11.01=1.101*2^1。

3、IEEE 754 規約形式

通過上述2步,得到1.101*2^1。我們採用規約形式獲取填充3個部分

s(Sign):正數=0;

E(Exponent):E=1,e=E+bias=1+127=128。此處儲存的e,是經過以127作為偏移量調整的。

M(Mantissa):1.101 中,我只獲取有效數(捨去整數部分1,只取小數部分)101


因此存在計算機中的3.25 浮點數是:


3、特殊值

從wiki上可以看到依據指數是否為0 ,還可以分為一下幾種情況

指數不全為0或者不全為1此時為規約形式,如上述的示例。尾數部分,預設整數部分是1,不儲存,獲取後第一位預設為1。指數部分有偏移量

E全為0 。此時為非規約形式。為何要引入非規則浮點數,當小於的數會下溢為0,對於高精度來說這是不能接受的,而引入不規則浮點數後,小於的數才會下溢為0 。

E全為1 。尾數為0,則表示無窮大。非零則表示NaN(浮點數排序這種特殊問題需要處理)


4、運算方式

一般浮點數的運算流程如下,非規則浮點計算加法時“對階”計算有不同,不再細說。

  1. 指數項對齊。指數項對齊,小的向大的對齊,如果判斷大小,則上述指數中的偏移量就起很大作用了,指數大的必然大,後續可以減少判斷
  2. 尾數求和。對齊後,對尾數進行加減處理
  3. 規則化。對尾數進行擷取,保證精度
  4. 舍入。判斷丟失的數值,進行舍入
  5. 判斷結果。判斷結果是否溢位


三、非規範化浮點數


1、為何要引入非規範化浮點數

引入一個精度失準的事故:

On 25 February 1991, a loss of significance in a MIM-104 Patriot missile battery prevented it intercepting an incoming Scud missile in Dhahran, Saudi Arabia, contributing to the death of 28 soldiers from the U.S. Army’s 14th Quartermaster Detachment.[25] See also: Failure at Dhahran

1991年2月25日,在MIM-104愛國者導彈電池中,一個重要的精準丟失阻止了它在沙烏地阿拉伯達哈蘭攔截一架新的飛毛腿導彈,造成美軍第十四軍區分離隊28名士兵死亡。

對於規則浮點數而言,指數項範圍為01-FE(1到254),當小於的浮點數,用規格化數值表示,運算的時候會被電腦當作0來處理,如果精度能夠再次提高一些的話,就不會出現這種情況了,因此引入不規則浮點數後,小於的數才會下溢為0 。

採用非規約浮點數,用來解決填補絕對值意義下最小規格數與零的距離,如上圖所示,僅僅用規則浮點數的表示方式,0到最小正常數之間的間隔要遠遠大於最小正常數到次小正常數之間的間隔(2^-126 * 2^-23 = 2^-149),可以說是非常突然的下溢位到0,這是不滿足我們的期望的。因此選擇約定小數點前一位可以為0,剩下的一小段區間(即黃色括號)再均勻劃分為段,如此就多了2^23精度,可以精確到附近。


2、引入非規範化浮點數帶來的問題

《你應該知道的浮點數基礎知識》引入一個演算法題,很好了詮釋導致計算速率方面的問題。我簡單貼一下:

 const float x=1.1;
 const float z=1.123;
 float y=x;
// 演算法1  
 for(int j=0;j<90000000;j++)
    {
        y*=x;
        y/=z;
        y+=0.1f;
        y-=0.1f;
    }

// 演算法2
for(int j=0;j<90000000;j++)
    {
        y*=x;
        y/=z;
        y+=0;
        y-=0;
    }複製程式碼

演算法2中在進行上百次迴圈以後,y被標識為非規格化浮點,最終導致的結果是演算法2比演算法1慢了整整7倍左右。

這就是非規則浮點數的計算速度慢於規則浮點數,雖然下溢下沉了,但需要CPU額外進行解碼和編碼標識,如此,效率緩慢,極端情況下,規格化浮點數操作可能比硬體支援的非規格化浮點數操作快100倍。

另外非規則浮點數無法解決計算過程中下溢的產生,因為只是精度精確到附近,當有更小的浮點數時候,依然會下溢為0。


相關文章