淺談浮點數(一)

Secondworld發表於2021-08-03

小數與浮點數

很多人都會認為,小數就是浮點數。但其實非也。
小數只是一種實數的一種特殊表現形式,所有分數都可以用小數來表示。
而浮點數,是計算機領域的一個術語,浮點數代表著目前計算機表示小數的一方式。

浮點數的由來

我們都知道計算機表示特定的資料型別長度是固定的。
比如在java語言裡,小數的表示,float是4位元組,double是8位元組。
那麼這些固定長度的二進位制位是如何表示小數的呢?
最直觀的表示辦法就是:固定的整數部分位數和固定的小數部分位數。比如以float為例,我們假設取前8位表示整數部分,後24位表示小數部分。則1.2用該方法表示如下:
	00000001 00000000 00000000 00000000 00000010
    以上這種表示小數的方法我們稱之為:定點表示法,即小數點的位置是固定的(這裡固定在第24位之前)。
但是這種定點表示法有一個很大的問題,就是表示數的範圍很有限。假設我現在要表示:256.1
那麼因為整數部分固定只有8位,將無法表示256,會出現溢位。

   於是乎聰明的電腦科學家想到了另一種辦法:科學計數法。我們知道10進位制下的科學計數法可以將一個數表示成: 1.xxx * 10^n 。
依葫蘆畫瓢,那麼2進位制的科學計數法應該長這樣:1.xxx * 2^n 
那麼我們在儲存小數的時候,可以用一部分儲存指數:n,一部分儲存小數:xxx 即可。
而這種表示的方式下,其實小數點沒有固定的位置,既小數點是浮動的。所以我們也就稱這種儲存方式下的數字為浮點數。

浮點數的儲存規範:IEEE 754

  IEEE二進位制浮點數算術標準(IEEE 754)是20世紀80年代以來最廣泛使用的浮點數運算標準,為許多CPU與浮點運算器所採用。
這個標準定義了表示浮點數的格式(包括負零-0)與反常值(denormal number)),一些特殊數值(無窮(Inf)與非數值(NaN)),以及這些數值的“浮點數運算子”;
它也指明瞭四種數值舍入規則和五種例外狀況(包括例外發生的時機與處理方式)。
說人話就是:一個浮點數可以表示如下:
	value = sign x exponent x fraction 
	其中value表示浮點數的實際值
	sign(bit)表示符號位: 0表示整數 1表示負數
	exponent表示的是轉換成科學計數法後的指數偏移值
	fraction表示小數部分
   知道浮點數的具體表示方式之後,接下來就是要確定每一部分所佔的長度。
在IEEE 754標準中,對於32位浮點數的各部分長度約定如下:
    ·1bit的sign + 8bit的exponent + 23bit的fraction·
而對於64位的浮點數的各部分長度約定如下:
    ·1bit的sign + 11bit的exponent + 52bit的fraction·

我們前面說過exponent並不是科學計數法之後的實際指數,而是代表科學計數法後的指數偏移量。那麼怎麼個偏移法呢?
其實在IEEE 754中也對這個做了規定。我們假設k表示exponent所佔的總位數,n表示轉換成科學計數法之後的實際指數值,那麼最終exponent = 2^(k-1) + n 
    
為什麼要這麼設計呢?我們知道小數可能是不帶整數的,這時候如果轉換成科學計數法之後實際指數值就應該是負數。
對於指數為負數的情況,我們很自然地會想到用exponent部分的第一位表示正負,然後對於負數值採用補碼的方式來表示(取反加一)。
而原來整個value值也有一個sign位表示正負,剩餘位在小數為負數的時候也需要使用補碼方式來表示。
我們假設這樣一種情況:指數為負數且小數為負數,那麼對exponent部分的兩次取反加1會導致最終結果不可預知。

	因此,最後IEEE 754採用了:exponent = 2^(k-1) + n 這種方式來儲存指數的偏移值。

java中如何檢視浮點數的二進位制表示

我們可以使用如下兩行程式碼來檢視0.1分別在32位和64位下的二級制形式:
     System.out.println(Integer.toString(Float.floatToIntBits(0.1f), 2)); // 111101110011001100110011001101
     System.out.println(Long.toString(Double.doubleToLongBits(0.1), 2)); // 11111110111001100110011001100110011001100110011001100110011010
我們將高位補0,並且按照前面所講的sign + exponent + fraction的形式將兩者拆解如下:
0 01111011    10011001100110011001101
0 01111111011 1001100110011001100110011001100110011001100110011010
要將一個小數轉換成浮點數的形式,首先要求得小數的二進位制表示法。0.1的整數部分為0,整數部分的如果用8位表示則為:00000000。
小數部分的0.1如何轉換成2進位制呢?這裡我們仍然要從10進位制小數來進行推導。

我們假設計算機是以10進位制的形式來儲存資料的。那麼對於0.631,小數部分第1位儲存的應該直接就是6,也就是0.631 * 10 的整數部分。
第2位儲存的應該就是3,也就是 0.31 * 10 (在第一步去掉整數部分之後再乘以10的整數部分)。同理第3位儲存的就是1,0.1 * 10。

於是乎我們可以得到0.1作為二進位制在計算機中的儲存:
	第一位: 0.1 * 2 = 0.2 的整數部分  0
	
	第二位: 0.2 * 2 = 0.4 的整數部分  0
	第三位: 0.4 * 2 = 0.8 的整數部分  0
	第四位: 0.8 * 2 = 1.6 的整數部分  1  ---》再去掉整數部分後為0.6
	第五位: 0.6 * 2 = 1.2 的整數部分  1  ---》再去掉整數部分後為0.2
	
	第六為: 0.2 * 2 = 0.4 的整數部分  0
	第七位: 0.4 * 2 = 0.8 的整數部分  0
	第八位: 0.8 * 2 = 1.6 的整數部分  1  ---》再去掉整數部分後為0.6
	第九位: 0.6 * 2 = 1.2 的整數部分  1  ---》再去掉整數部分後為0.2
	
	.....
	
綜上,我們得到0.1的二進位制儲存應該為:0001100110011...(0011迴圈)。
於是,0.1的整個二進位制表示為: 00000000.0001100110011...(0011迴圈)
轉換成科學計數法為:1.100110011...(0011迴圈) * 2^(-4)。
按照IEEE 754標準,如果是32位的表示法,那麼exponent = 2 ^ 7 + (-4) = 01111011
如果是64位表示法,則exponent = 2 ^ 10 + (-4) = 01111111011
再按照 sign + exponent + fraction的表示方法拼接起來即得到32位和64位的表示分別如下:
	0 01111011    100110011001100110011...(0011迴圈)
	0 01111111011 100110011001100110011001100110011001100110011...(0011迴圈)

最後剩下的問題就是:小數的儲存位數是固定的,那麼如果將迴圈的部分截斷呢?這就涉及到舍入規則。
舍入的規則如下:即如果左規或右規時丟棄的是0,則捨去不計,反之要將尾數的末尾加1。
我們同樣以0.1為例,32位情況下,小數部分的最終表示如下:10011001100110011001101
我們知道小數部分最後是0011迴圈,所以最後一位數字本來應該是0,但是因為緊接著的是1,所以最終擷取之後還需要進行加1操作,於是就得到1。
64位的表示法同樣也可以根據這個規則得到。