浮點數那些事兒

本文爲做者本身的總結的,因爲做者的水平限制,不免會有錯誤,歡迎你們指正,感激涕零。函數

提及浮點數,你們都是又恨又愛的。愛呢,是由於,只有它能夠方便地使用小數;恨呢,是由於它並不能精確地表示小數。3d

以 PHP 爲例:floor((0.1 + 0.7) * 10) 這樣一個函數調用,根據數學老師死得晚原理,你們都能得出 8 這個結果。但是事實上呢?它會返回 7。數學老師的棺材板。。。(╯‵□′)╯︵┻━┻code

但是爲何會出現這種狀況呢?這就要從浮點數的特性提及了。內存

萬物皆二進制

咱們都知道,在計算機中,一切的一切都是二進制表示的。假設一個 4 字節整型的十進制數 8,在大端表示的機器中,表示成 00000000 00000000 00000000 000010000x0000008)。將十進制整數轉換成二進制數,是很是容易的。但是,小數呢?好比,咱們要表示 1.75,該怎麼存儲在計算機中呢?顯然,不能像整數同樣存儲了。數學

小數的二進制

讓咱們回憶一下,在十進制中,小數是怎麼計算的。上面的 1.75 咱們是這麼算的:1 × 10^0 + 7 × 10^-1 + 5 × 10^-2 。那麼咱們按照相同的規則,來用二進制計算一下小數部分:0.75 = 1/2 + 1/4,也就是 1 × 2^-1 + 1 × 2^-2 ,再加上前面的整數部分,那麼整個式子就變成了 1 × 2^0 + 1 × 2^-1 + 1 × 2^-2 ,寫成二進制形式就是 1.11。因此,1.75 的二進制表示是 1.11。原理

對於將小數轉換爲二進制,和整數部分除二取餘相反的,是乘二取整。二進制

0.75 * 2 = 1.5 -> 1
0.5 * 2 = 1 -> 1float

因此咱們一樣能夠得出 1.11。d3

科學計數法

好了,咱們已經知道如何表示一個小數的二進制了。辣麼,問題來了。學過 C 語言的同窗都知道,一個 float 只有 4 字節,一個 double 也只有 8 字節。那麼,這麼表示一個小數,好像範圍頗有限。總結

在數學老師哭暈在廁所以前,咱們應該還記得十進制數中有這麼一個東西——科學計數法,咱們能夠很方便地用它來表示很大的十進制數。那麼,同理,咱們也能夠用在浮點數的表示上。

讓咱們先來回憶一下,科學計數法的表示。假設咱們有一個數 17500,咱們能夠用科學計數法表示成 1.75 × 10^4 。咱們照葫蘆畫瓢,在二進制數中,假設有一個數是 11010。咱們來和十進制對應一下。十進制是乘 10,那麼二進制就是乘 2,咱們對應的就能夠寫成 1.101 × 2^100 。對,其實就是這麼簡單。那也許有的人會問了,爲何不寫成 0.1101 × 2^101 呢?咱們再來回憶一下,在十進制科學計數法中,是否是有一個規定,整數部分的範圍是 [1,10)。那對應到咱們的二進制數上,這個規定就能夠變成 [1,2) 了,沒錯,對應關係就是這麼簡單。

浮點數

好了,咱們如今也知道怎麼使用二進制來表示小數,以及使用科學計數法來表示二進制小數了。那麼,咱們距離把數字存入計算機內存僅剩一步之遙了,咱們要把全部的東西存到內存裏去,那麼咱們就須要合理地分配內存空間。浮點數有兩種,一種是單精度浮點數(float),佔用 4 字節的內存。其中,1 位是符號位,8 位是階碼(冪),23 位是尾數(小數部分)。

細心的各位可能會發現,好像沒有整數部分?別急,這就是上面那個規定的有用之處。當整數部分在 [1,2) 之間時,也就只可能取到一個值 1,那麼,對於這個值,咱們是否是就能夠當作默認值而不記錄在浮點數的表示中了?而這樣,咱們的浮點數的精度又多了一位(小數部分的位數決定了精度)。這種表示叫作隱含 1 開頭的表示。

規格化與非規格化

偏置值

到了這裏,咱們發現,第一位是浮點數的正負符號,那麼,對於一個科學計數法來講,階碼一樣須要有正負。而在單精度中,階碼只有 8 位;雙精度中,階碼只有 11 位。若是咱們給階碼錶示成補碼,那麼,咱們可以表示的數的範圍就會縮小,這樣顯然是不划算的。因而,偏置值就由此誕生了。

規格化的值(階碼不全爲 0 或 1)

在內存中的規格化的浮點數表示中,階碼並不是是 2 的冪,而是通過計算的結果,這個計算公式就是 e - Bias,這裏的 Bias 就是偏置值,而 e 就是階碼在浮點數中的二進制表示。Bias 的值是 2^k-1 - 1(單精度是 127,雙精度是 1023),因此,e - Bias 的取值範圍就是 [-126, 127](單精度)和 [-1022, 1023](雙精度)。其實若是對補碼瞭解的比較好的同窗,應該就能看出來,這其實就是省略了符號位的補碼錶示)。

經過上面的隱含 1 開頭的表示的尾數,咱們能夠計算出基數 M = 1 + f。那麼咱們整個的浮點數能夠寫成這樣一個表達式:M × (e - Bias)。

非規格化的值(階碼全爲 0)

對於規格化和非規格化的值來講,咱們均可以用同一個式子來表示。不過,爲了某些更加方便的緣由(這裏就不展開講了),對它們作了區分。若是按照規格化的計算來看,階碼的值是 0 - Bias,不過在這裏,咱們讓階碼的值等於 1 - Bias。一樣的,因爲咱們給階碼加了 1,那麼整個浮點數就會向左移動一位,那麼,咱們須要讓浮點數的值不變,M 就不在須要上面整數部分的 1 了,因此 M = f

同時,咱們會發現一個問題,那就是 +0.0 和 -0.0 在浮點數的二進制表示上是不一樣的。

特殊值(階碼全爲 1)

最後,還剩下這樣一種數字,那就是階碼全爲 1 的狀況。當小數爲 0 的時候,浮點數的值爲 ∞。當小數不爲 0 時,浮點數的值爲 NaN,即不是一個數(Not a Number)。

計算浮點數

好了,扯了這麼多,咱們如今回到最開始的問題上,floor((0.1 + 0.7) * 10) = 7。咱們先看 0.1 的二進制表示。

首先,咱們將十進制小數轉換成二進制小數,能夠獲得 0.000[1100]···。讓咱們轉換成浮點數的二進制表示。按照上面的規則,它能夠被表示成科學計數法 1.10011001100110011001100 × 2^-4 ,這樣,階碼就是 -4 + 127 = 123,二進制表示爲 01111011。因此,整個浮點數的二進制表示就是 00111101110011001100110011001100(0x3dcccccc)。一樣的,0.7 會表示爲00111101001100110011001100110011(0x3d333333)。

首先咱們要對階碼小的數進行對階,而後再進行尾數的加法,這樣,咱們獲得的值就是 00111101111001100110011001100101。咱們將其轉換成十進制,發現,它是小於 0.8 的。所以,當咱們再進行乘法運算向下取整時,會等於 7。

最後

其實,浮點數有不少坑。所以,咱們在使用浮點數的時候,必定要當心。還有,涉及到金額計算的時候,必定不能使用浮點數。

本文爲做者本身讀書總結的文章,因爲做者的水平限制,不免會有錯誤,歡迎你們指正,感激涕零。

參考文獻

《深刻理解計算機系統(第 3 版)》第 2.4.2 節

相關文章
相關標籤/搜索