1. 程式人生 > 其它 >浮點數運算的一些問題

浮點數運算的一些問題

引入

先來看個程式碼:

print(1-0.7 == 0.3)

很多人會覺得這一看不就是True嗎,但實際上結果為False。因為1-0.7的結果為0.30000000000000004

浮點數轉二進位制的方法

可以用這個網站驗證答案:https://c.runoob.com/front-end/58/

因為所有的資料本質都是通過二進位制數儲存的,所以要分析這個問題的本質,我們得先去看浮點數的二進位制表示。因為整數和小數的轉換方法不同,所以先將十進位制數的整數部分和小數部分分別轉換後,再加以合併。

(1)整數部分:整數部分比較簡單,因為一個有限的十進位制整數都能轉換成有限的二進位制數。採用除2取餘,逆序連線的方法,程式碼如下:

n = 173
ls = []
while n != 0:
    ls.append(str(n%2))
    n//=2
ls.reverse()
print(''.join(ls))

(2)小數部分:小數部分會麻煩一點了,具體的方法為:

'''
如:0.625=(0.101)bin
0.625*2=1.25======取出整數部分1
0.25*2=0.5========取出整數部分0
0.5*2=1==========取出整數部分1
直到積中的小數部分為零為止,從上倒下連線
'''
import math

n = 0.625
s = ''
while int(n)!= n:
    n*=2
    s+=str(int(n))
    n = math.modf(n)[0]
print(s)

在這裡可以發現,十進位制的小數轉二進位制的時候,很容易就會無限迴圈下去,例如0.2轉換成二進位制就i是0011001100110011001100……,由於計算機儲存的位是有限的,所以必然會造成精度的損失。

IEEE 754 

這裡就不得不提到IEEE 754 了,這東西其實就是一個浮點數的運算標準。因為對於有符號整數來說,儲存的方式其實很簡單,一位表示符號,剩下的位都可以用來表示數值,但浮點數的情況比較複雜。對於float32,也就是32位的浮點數,是這樣表示的:V = (-1)^S*M*2^E

 (1)(-1)^s 表示符號位,當 s=0,V 為正數;當 s=1,V 為負數

 (2)M稱為尾數,1≤ M <2,也就是說,M 可以寫成 1.xxxxxx 的形式,其中 xxxxxx 表示小數部分。

 (3)E表示階碼。例如浮點數5.0,用二進位制表示是101.0,用科學計數法表示為1.01*2^2(注意,因為這裡是二進位制數,所以是乘2的2次方而不是10的二次方)。此時階碼E就是2,尾數M為1.01

對於 32 位的浮點數,最高的 1 位是符號位 s,接著的 8 位是指數 E,剩下的 23 位為有效數字 M。

尾數

因為M 總是寫成 1.xxxxxx *2^E的形式,所以在計算機內部儲存 M 時,預設這個數的第一位總是 1,因此可以被捨去,只儲存後面的 xxxxxx 部分。比如儲存 1.01 的時候,只儲存小數部分的 01,等到讀取的時候,再把第一位的1加上去。這樣做的目的,是節省 1 位有效數字。以32位浮點數為例,留給 M 只有 23 位,將第一位的1捨去以後,等於可以儲存 24 位有效數字。

階碼

上面說到了,階碼是一個八位的二進位制數,並且它是一個無符號數,所以取值範圍是[0,255]。但是科學計數法中的指數並不一定都是正數,例如十進位制0.5轉換為二進位制是0.1,因為1<=M<2,所以要寫成1*2^-1,所以階碼部分,採用移碼來表示。簡單地講,就是階碼的真實值為計算機儲存值減去中間數127,比如,2^10 的 E 是 10,所以儲存成 32 位浮點數時,必須儲存成 10+127=137,即 10001001,這樣階碼的範圍就變成了[-127,128]。階碼這裡又有一些特殊情況:

(1)階碼E全為0,有效數字 M 不再加上第一位的 1,而是還原為 0.xxxxxx 的小數。這樣做是為了表示機器零(計算機中小到機器數的精度達不到的數均視為“機器零”),以及接近於 0 的很小的數字。

(2)階碼E全為1,階碼全為1的情況有兩種,用來表示特殊值,第一種是尾數M也全為1,這時表示正負無窮大(正還是負取決於符號位)。若M不全為1,則表示NaN(Not a Number,非數,這個概念也是IEEE 754定義的),例如0除以0的結果。

 遺留的問題

現在我們就可以解釋為什麼1-0.7的結果是0.30000000000000004了,由於計算機儲存浮點數的位數有限,所以浮點數轉成二進位制表示的時候,必然會造成精度的損失,例如0.3轉換為二進位制表示為:

0.0100110011001100110011001100110011001100110011001101(舍入後的的結果),而把這一串再轉換為十進位制就是0.30000000000000004

但這裡還有一個問題,既然計算機都是二進位制儲存的,那麼1-0.7和0.3應該同時存在舍入,那麼它們倆應該還是相等的,但事實並非如此。而且,使用加減法很容易出問題,但乘除法不會。

print(1-0.9)#0.09999999999999998
print(0.2/2)#0.1

至於是為什麼,暫時還搞不清楚,以後來填坑