浮點數加法引發的問題:浮點數的二進位制表示
1、問題:
之前有同學問過這樣一個問題:
echo|awk '{print 3.99 -1.19 -2.80}'
4.44089e-16
類似的問題還有在 java 或者 javascript 中:
23.53 + 5.88 + 17.64 = 47.05
23.53 + 17.64 + 5.88 = 47.050000000000004
為什麼結果不是 0 或者不相等呢?
如果你不能立馬回答出原因,那說明你對浮點數計算的基本知識還不瞭解。
剛好最近 segmentfault.com 上也有同學問了同樣的一個問題,現在整理下,以備忘。
2、浮點數的概念:
浮點數是屬於有理數中某特定子集的數的數字表示,在計算機中用以近似表示任意某個實數。具體的說,這個實數由一個整數或定點數(即尾數)乘以某個基數(計算機中通常是2)的整數次冪得到,這種表示方法類似於基數為10的科學記數法。
浮點計算是指浮點數參與的運算,這種運算通常伴隨著因為無法精確表示而進行的近似或舍入。
3、十進位制到二進位制的轉化問題:
為了更好的理解,先來看一下10進位制的純小數是怎麼表示的,假設有純小數D,它小數點後的每一位數字按順序形成一個數列: {k1,k2,k3,...,kn} 那麼D又可以這樣表示: D = k1 / (10 ^ 1 ) + k2 / (10 ^ 2 ) + k3 / (10 ^ 3 ) + ... + kn / (10 ^ n ) 推廣到二進位制中,純小數的表示法即為: D = b1 / (2 ^ 1 ) + b2 / (2 ^ 2 ) + b3 / (2 ^ 3 ) + ... + bn / (2 ^ n ) 現在問題就是怎樣求得b1,b2,b3,……,bn。演算法描述起來比較複雜,還是用數字來說話吧。宣告一下,1 / ( 2 ^ n )這個數比較特殊,我稱之為位階值。
例如0.456,第1位,0.456小於位階值0.5故為0;第2位,0.456大於位階值0.25,該位為1,並將0.456減去0.25得0.206進下一位;第3位,0.206大於位階值0.125,該位為1,並將0.206減去0.125得0.081進下一位;第4位,0.081大於0.0625,為1,並將0.081減去0.0625得0.0185進下一位;第5位0.0185小於0.03125…… 最後把計算得到的足夠多的1和0按位順序組合起來,就得到了一個比較精確的用二進位制表示的純小數了,同時精度問題也就由此產生,許多數都是無法在有限的n內完全精確的表示出來的,我們只能利用更大的n值來更精確的表示這個數,這就是為什麼在許多領域,程式設計師都更喜歡用double而不是float。
4、解釋:
對於開頭的問題,我們再舉幾個例子(以下的例子採用 python 做示範):
>>> 0.125
0.12500000000000000
>>> 0.1
0.10000000000000001
>>> 0.6 + 0.1
0.69999999999999996
納尼?什麼會這樣? 0.125,也就是 1/8,的二進位制,是 0.001,可以在 10 進位制和 2 進制中輕鬆表達。 但 0.1 就是一個經典的頭疼數字了,它的二進位制,是 0.00011001100110011001100110011001...,一個無限迴圈小數。由於計算機中使用的浮點數是基於有限精度的二進位制數,因此,不可能絕對準確。這一現象往往在列印浮點數時才被注意到。 浮點數的二進位制表示,一般採用 IEEE 754 標準。標準規定:單精度格式具有 24 位有效數字,共 32 位。雙精度格式具有 53 位有效數字精度,共 64 位。 但是,如今的直譯器和 print 函式都足夠聰明,會在列印浮點數的時候自動舍入,但是又有一些浮點數由於誤差過大,又不能捨入。 因此造成了“有些浮點數計算是對的,有些是錯的”的現象。事實上,所有的浮點數運算都是“錯”的。也就是你問題的答案。同時,這可能會成為除錯程式的煙幕彈:“哎?print 出來就是 0.1,為什麼計算的時候會出現問題?” 例如,新版本的 Python 預設對所有的浮點數進行自動舍入。因此無法重現我在文首的例子。這時,可以使用
>>> print("%.17lf" % (0.6 + 0.1))
0.69999999999999996
同理,浮點數之間用 >, <, == 來比較大小是不可取的。需要看兩個浮點數是否在合理的誤差範圍,如果誤差合理,即認為相等。 另外一個陷阱是,浮點數的誤差會累積。
x = 0.0
for i in range(100):
x += 0.1
print("%.17lf" % x) #=> 9.99999999999998046
print(x) #=> 99.1,print 自動舍入,得到了看似正確的結果
在一般計算中,處理二進位制浮點數需要用到很多技巧和技術。但在財務等運算中,必須要求完全精確的結果,這時候,需要模擬 10 進位制的浮點數。如 Python 中提供了 Decimal 模組,允許使用者傳入浮點數的字串進行模擬計算,避免精度問題。
from decimal import Decimal
x = Decimal("0.0") # 注意:傳入字串。如果傳入浮點數,那麼在計算之前精度就損失掉了
for i in range(100):
x += Decimal("0.1")
print("%.17lf" % x) #=> 10.00000000000000000
print(x) #=> 10.0
關於 IEEE 浮點數,浮點數的大小比較等具體演算法和細節,可以觀看網易上麻省理工學院的這一集課程:
http://v.163.com/movie/2010/6/4/1/M6TCSIN1U_M6TCT0L41.html ,可以從 05:39 處開始觀看。
5、結論
這就是為什麼交易系統的價格,金錢都不會使用float,double,包括資料庫的儲存。例如:mysql 可以用 decimal ,如果你是用 java, 在商業計算中我們要用 java.math.BigDecimal,注意:如果需要精確計算,非要用String來夠造BigDecimal不可!或者 sprintf 進行精度舍入。另外有些語言專門提供了處理金融資料的型別。
6、REFER:
1、http://zh.wikipedia.org/zh-cn/IEEE_754
2、http://baike.baidu.com/view/339796.htm
3、http://www.ruanyifeng.com/blog/2010/06/ieee_floating-point_representation.html
4、http://segmentfault.com/q/1010000000267988
5、http://www.laruence.com/2013/03/26/2884.html PHP浮點數的一個常見問題的解答