浮點數加法引起的問題:浮點數的二進制表示

一、問題:

以前有同窗問過這樣一個問題: javascript

echo|awk '{print 3.99 -1.19 -2.80}'
4.44089e-16

相似的問題還有在 java 或者 javascript 中: html

23.53 + 5.88 + 17.64 = 47.05

23.53 + 17.64 + 5.88 = 47.050000000000004

爲何結果不是 0 或者不相等呢? java

若是你不能立馬回答出緣由,那說明你對浮點數計算的基本知識還不瞭解。 python

恰好最近  segmentfault.com 上也有同窗問了一樣的一個問題,如今整理下,以備忘。 mysql

二、浮點數的概念

浮點數是屬於有理數中某特定子集的數的數字表示,在計算機中用以近似表示任意某個實數。具體的說,這個實數由一個整數或定點數(即尾數)乘以某個基數(計算機中一般是2)的整數次冪獲得,這種表示方法相似於基數爲10的科學記數法。 程序員

浮點計算是指浮點數參與的運算,這種運算一般伴隨着由於沒法精確表示而進行的近似或舍入。 算法

三、十進制到二進制的轉化問題:

爲了更好的理解,先來看一下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 )這個數比較特殊,我稱之爲位階值。 sql

例如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。 shell

四、解釋:

對於開頭的問題,咱們再舉幾個例子(如下的例子採用 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 處開始觀看。

五、結論

這就是爲何交易系統的價格,金錢都不會使用float,double,包括數據庫的存儲。例如:mysql 能夠用 decimal ,若是你是用 java, 在商業計算中咱們要用 java.math.BigDecimal,注意:若是須要精確計算,非要用String來夠造BigDecimal不可!或者 sprintf 進行精度舍入。另外有些語言專門提供了處理金融數據的類型。

六、REFER:

一、http://zh.wikipedia.org/zh-cn/IEEE_754

二、http://baike.baidu.com/view/339796.htm

三、http://www.ruanyifeng.com/blog/2010/06/ieee_floating-point_representation.html

四、http://segmentfault.com/q/1010000000267988

五、http://www.laruence.com/2013/03/26/2884.html  PHP浮點數的一個常見問題的解答

相關文章
相關標籤/搜索