3.7.1.4浮點數運算
要討論浮點數運算,牽涉到的知識比較多,下面一點一點的來逐步展開。為了便於同時討論十進位制和二進位制數,我們做一個約定,我們把十進位制數簡寫為N10,把二進位制數簡寫為N2。
3.7.1.4.1小數的二進位制
前面我們學過了二進位制,但是都是侷限於整數,那麼小數的二進位制是怎麼樣的?其實二進位制的小數和十進位制類似,例如1101.01012。那麼這個二進位制轉化為十進位制是多少呢?還記得二進位制轉十進位制的公式嗎?
N=an*2n-1 + an-1*2n-2 + … + a1*20
對於小數部分,我們可以繼續擴充一下,變成:
N=an*2n-1 + an-1*2n-2 + … + a1*20 + b1*2-1 + b2*2-2 + … + bm*2-m
b表示小數點後面的數字。那麼:
1101.01012=23 + 22 + 20 + 2-2 + 2-4 = 8 + 4 + 1 +0.25 + 0.0625 = 13.312510
好了,到此我們已經知道了整數和小數的二進位制表示以及轉化為十進位制的方法。那麼對於一個十進位制數,怎麼轉化為二進位制呢?我們先看十進位制整數的轉化。對於一個十進位制整數,我們知道公式:
N10=an*2n-1 + an-1*2n-2 + … + a1*20
我們把N10不斷的除以2,即
N10/2=(an*2n-1 + an-1*2n-2 + … + a1*20)/2= an*2n-2 + an-1*2n-3 + … + a2*20,餘a1
N10/4= an*2n-3 + an-1*2n-4 + … + a3*20,餘a2
……
最終,會餘an 。然後我們把每一次的餘數從右往左排列,anan-1……a1,其實就是對應的二進位制數。因此對於一個十進位制整數,我們只需要不斷的除以2,把餘數記錄下來,然後從右往左排列,即為二進位制。我們稱之為“除2取餘,逆序排列法”,例如對於1310
13/2,商6,餘1
6/2,商3,餘0
3/2,商1,餘1
1/2,商0,餘1
所以1310=11012
對於十進位制小數,公式為:
M10= b1*2-1 + b2*2-2 + … + bm*2-m
由於指數是負數,所以我們採用不斷乘以2(整數部分不乘),得到下面序列:
b1+ b2*2-1 + … + bm*2-(m-1),b1是一個整數
b2+ … + bm*2-(m-2) ,b2是一個整數
……
bm
我們把b1到bm排列起來,就是對應的二進位制小數部分。,舉例:
0.3125*2=0.625,整數部分是0
0.625*2=1.25,整數部分是1
0.25*2=0.5,整數部分是0
0.5*2=1,整數部分是1
因此0.312510=0.01012
3.7.1.4.2科學記數法
我們知道,把一個十進位制數的用科學記數法(scientific notation)可以表示為a*10n或者aEn,其中0<=|a|<10,n是自然數。例如:
118.0625=1.180625*102=1.180625E2
0.0375=3.75*10-2=3.75E-2
對於二進位制,我們同樣可以採用類似的科學記數法,只不過把10換成2,例如:
0.00101=1.01*2-3
我們可以把科學記數法看成由3個部分組成:符號部分、有效數字部分、指數部分,示意圖如下:
3.7.1.4.2IEEE754標準
我們在討論浮點型的時候,提到過float和double的運算都遵循IEEE754標準,當時大家肯定想知道,什麼是IEEE754標準。這裡不打算對這個標準做全盤講解,只嘗試通過通俗的語言和簡單的示例,讓大家理解和明白浮點數時如何儲存的,浮點數是如何計算的。
先來討論一下浮點數是如何儲存的。IEEE 754規定了四種表示浮點數值的方式:單精確度(32位)、雙精確度(64位)、延伸單精確度(43位元以上,很少使用)與延伸雙精確度(79位元以上,通常以80位實現)。只有32位模式有強制要求,其他都是選擇性的。Java中使用了前2種。在記憶體中,是採用科學計數法存放的,分別對應科學計數法的3個部分:符號部分、有效數字部分和指數部分,具體規定為:
float:符號(1bit)、指數(8bit)、有效數字(23bit)
double:符號(1bit)、指數(11bit)、有效數字(52bit)
其中符號位0代表正,1代表負
示意圖如下:
下面我們以單精度浮點數0.15625講解浮點數的儲存過程:
0.15625轉化為二進位制就是0.00101,然後將該數寫成科學計數法:
0.15625 = 0.00101 = 1.01 * 2-3
有效數字部分是1.01,指數部分是-3。接下來就是把1.01和-3變成二進位制,存放到對應的位置就可以了。但是這裡有2個問題:
- 對於二進位制來說,有效數字部分的整數部分只能是1,是不是可以不用存?
- 指數部分是-3,這是十進位制數,前面我們學習過用補碼錶示負數,這裡也用補碼嗎?
對於第1個問題,IEEE754標準規定,整數部分的的1不用存(讀取的時候再補上,相當於有效數部分左邊有一個隱藏位,值為1),這樣可以節省一個bit的空間。因此只需要存放.01中的01即可,變成二進位制為:010 0000 0000 0000 0000 0000
對於第2個問題,採用補碼存放,理論上沒有問題,但是IEEE754標準採用了另一種方法。我們知道float採用8位存放指數,範圍是0~255,但是因為指數是有正負的,因此規定一個偏移量127,真正的指數值等於存放的值減去這個偏移量。這樣一來,指數的範圍就變成-127~128了。double採用11位存放指數,範圍是0~2048,偏移量是1023,指數範圍是-1023~1024。
我們用float存放,因此例子中的指數-3,存放的時候需要加上偏移量,變成124,二進位制為(補足8位):0111 1100。整個存放的示意圖如下:
接下來,我們再聊一聊IEEE754種對於2種特殊值的規定:非數值NaN和無窮大infinity。
前面我們介紹過,正負浮點數除以0得到正負無窮大,浮點數0除以0,會得到一個NaN。那麼無窮大和NaN是怎麼表示的呢?IEEE754標準規定如下:
無窮大(infinity):
- 符號位0表示正無窮大,1表示負無窮大
- 偏移指數位全為1
- 有效數位全為0
NaN:
- 符號位0或1均可
- 偏移指數位全為1
- 有效數位只要不全為0(全為0表示無窮大)
前面我們說過有效數部分左邊有一個隱藏位,預設為1,那麼問題來了,浮點數0怎麼儲存?因此IEEE754對0做了特殊規定:
零(Zero):
- 符號位0或1均可
- 偏移指數位全為0
- 有效數全為0
前面我們說過有效數部分左邊有一個隱藏位,預設為1。但其實並非如此,IEEE754規定了一種特殊情況:
非正規數(Denormalized Number)
- 符號位0或1均可
- 偏移指數位全為0
- 有效數位不全為0(全為0表示0)
對於非正規數,有效數位的隱藏位視為0,並且偏移指數的是單精度是-12610(並非-127),雙精度是-1022(並非-1023)
我們可以看到,當偏移指數全為0或全為1的時候,都是特殊情況。
下面我們來研究一下精度問題,我們先看下0.3的儲存情況:
0.3*2=0.6,整數部分是0
0.6*2=1.2,整數部分是1
0.2*2=0.4,整數部分是0
0.4*2=0.8,整數部分是0
0.8*2=1.6,整數部分是1
0.6*2=1.2,整數部分是1
因此0.310 = 0.0 1001 1001 1001 1001 1001 1001 1001……2,科學計數法為:
0.310 = 1.001 1001 1001 1001 1001 1001 1001…… * 2-2(紅色表示小數點後第24位),單精度有效數只有23位,需要舍入(第24位是1,後面不全為0,需要進1,具體舍入規則這裡不解釋了)為:1.001 1001 1001 1001 1001 1010 * 2-2,偏移指數為-2+127=125,二進位制為01111101,因此單精度儲存為:
0 01111101 00110011001100110011010
再還原成十進位制為0.30000001192092896,因此浮點數存在精度問題。因此對於精度要求比較高的計算,不推薦使用浮點數進行運算。