0.28+0.34=? 一個簡單小數加法引發的思考
0.28+0.34=?
我相信這個簡單的加法,誰都會,肯定等於0.62嘛。
這是兩個特別簡單的加法,那如果我在其整數位置上加上其他的數字,或者多加幾個和項,你是否還能快速算過來?
我想這時候,我們又得藉助計算器了!而這,有時可能就是電腦!尤其是如果咱們藉助簡單程式語言來算的時候,嘿嘿,可能就不是那麼回事了~
不信你看,用javascript算的結果:
用python算的結果:
Line"/>
當然了,我嘗試著用其他語言來試一下,結果好像並不都是這樣。
其中,java只會在型別轉換的時候出現奇怪的值:(當然這在我們寫程式碼時往往很容易這麼幹)
好了,前言就到此為止!咱們是要來看一下,為什麼 1+1不等於2 ?
其實這是由浮點數在計算機中的儲存方式決定的,因為計算機只認識0101,所以小數點的儲存就需要使用另外的演算法來轉換了,大概如下:(以下內容參考網路知識庫)
計算機中是用有限的連續位元組儲存浮點數的。 儲存這些浮點數當然必須有特定的格式, C/C++中的浮點數型別 float 和 double 採納了 IEEE 754 標準中所定義的單精度 32 位浮點數和雙精度 64 位浮點數的格式。 在 IEEE 標準中,浮點數是將特定長度的連續位元組的所有二進位制位分割為特定寬度的符號域,指數域和尾數域三個域, 其中儲存的值分別用於表示給定二進位制浮點數中的符號,指數和尾數。 這樣,通過尾數和可以調節的指數(所以稱為"浮點")就可以表達給定的數值了。
32位浮點數儲存結構如下:
三個主要成分是:
- Sign(1bit):表示浮點數是正數還是負數。0表示正數,1表示負數
- Exponent(8bits):指數部分。類似於科學技術法中的M*10^N中的N,只不過這裡是以2為底數而不是10。需要注意的是,這部分中是以2^7-1即127,也即01111111代表2^0,轉換時需要根據127作偏移調整。
- Mantissa(23bits):基數部分。浮點數具體數值的實際表示。
根據國際標準IEEE 754,任意一個二進位制浮點數V可以表示成下面的形式:
V = (-1)^s×M×2^E
(1)(-1)^s表示符號位,當s=0,V為正數;當s=1,V為負數。
(2)M表示有效數字,大於等於1,小於2,但整數部分的1可以省略。
(3)2^E表示指數位。
比如:
對於十進位制的5.25對應的二進位制為:101.01,相當於:1.0101*2^2。所以,S為0,M為1.0101,E為2。
而-5.25=-101.01=-1.0101*2^2.。所以S為1,M為1.0101,E為2。
來看另一篇文章的簡單解說(https://www.cnblogs.com/yiyide266/p/7987037.html):
Step 1 改寫整數部分
以數值5.2為例。先不考慮指數部分,我們先單純的將十進位制數改寫成二進位制。
整數部分很簡單,5.即101.。
Step 2 改寫小數部分
小數部分我們相當於拆成是2^-1一直到2^-N的和。例如:
0.2 = 0.125+0.0625+0.007825+0.00390625即2^-3+2^-4+2^-7+2^-8….,也即.00110011001100110011。
或者換個更傻瓜的方式去解讀十進位制對二進位制小數的改寫轉換,通常十進位制的0.5也(也就是分數1/2),相當於二進位制的0.1(同等於分數1/2),
我們可以把十進位制的小數部分乘以2,取整數部分作為二進位制的一位,剩餘小數繼續乘以2,直至不存在剩餘小數為止。
例如0.2可以轉換為:
0.2 x 2 = 0.4 0
0.4 x 2 = 0.8 0
0.8 x 2 = 1.6 1
0.6 x 2 = 1.2 1
0.2 x 2 = 0.4 0
0.4 x 2 = 0.8 0
0.8 x 2 = 1.6 1
.......
即:.0011001.......(它是一個4862的無限迴圈的二進位制數,明白為什麼十進位制小數轉換成二進位制小數的時候為什麼會出現精度損失的情況了嗎)
Step 3 規格化
現在我們已經有了這麼一串二進位制101.00110011001100110011。然後我們要將它規格化,也叫Normalize。其實原理很簡單就是保證小數點前只有一個bit。於是我們就得到了以下表示:1.0100110011001100110011 * 2^2。到此為止我們已經把改寫工作完成,接下來就是要把bit填充到三個組成部分中去了。
Step 4 填充
指數部分(Exponent):之前說過需要以127作為偏移量調整。因此2的2次方,指數部分偏移成2+127即129,表示成10000001填入。
整數部分(Mantissa):除了簡單的填入外,需要特別解釋的地方是1.010011中的整數部分1在填充時被捨去了。因為規格化後的數值整部部分總是為1。那大家可能有疑問了,省略整數部分後豈不是1.010011和0.010011就混淆了麼?其實並不會,如果你仔細看下後者:會發現他並不是一個規格化的二進位制,可以改寫成1.0011 * 2^-2。所以省略小數點前的一個bit不會造成任何兩個浮點數的混淆。
好了,看完上面的浮點數的儲存原理後,是時候來解答,為什麼計算機會算錯的問題了!
1. 遇到小數點後數字轉換為實際儲存結構時,有的轉換是一個死迴圈,即不可能得到一個精確的值,而這個不精確的值再與其他資料做運算時,得到的結果自然也就可能存在差距了。至於有時候能得到準確的數值,有時候卻得不到準備的值,則是和逆轉換相關了(即記憶體結構轉換為可視的十進度資料)!
2. 另一個存在誤差的原因,則是因為在計算過程中進行了資料型別的轉換,因為原資料本來就不是精確的值,所以在進行型別轉換後,就不會得到和原始值直接轉化的值的相同結果了。
所以,咱們在做需要高精度的計算場合時,使用計算機語言自帶的儲存結構可能會不滿足咱們的需求,當然這也很容易辦到,一般也會有第三方的解決方案,即換一種儲存結構就可能能解決這種問題了。
如 java 中,使用 BigDecimal 來解決需要高精度運算的場景。(BigDecimal的解決方案就是,不使用二進位制,而是使用十進位制(BigInteger)+小數點位置(scale)來表示小數);BigDecimal應使用string構造更為準確,否則會在第一步轉換時出現精度丟失!
最後,附幾個加法結果以供參觀:
>> 57168.619999999995-11087.28 46081.34 >> 2412.02+11087.64+8338.28+5580.0 27417.940000000002 >> 0.28+0.34 0.6200000000000001 >> 2.28+2.34 4.619999999999999 >> 33.28+3.34 36.620000000000005 >> 3.28+3.34 6.619999999999999 >> 4.28+4.34 8.620000000000001 >> 5.28+5.34 10.620000000000001 >> 8.28+8.34 16.619999999999997 >> 33.28+9.34 42.620000000000005