0.28+0.34=? 一個簡單小數加法引發的思考 原

摘要: 浮點數不能隨便加啊。

Fundebug經授權轉載,版權歸原作者所有。

0.28+0.34=?

我相信這個簡單的加法,誰都會,肯定等於0.62嘛。

這是兩個特別簡單的加法,那如果我在其整數位置上加上其他的數字,或者多加幾個和項,你是否還能快速算過來?

我想這時候,我們又得藉助計算器了!而這,有時可能就是電腦!尤其是如果咱們藉助簡單程序語言來算的時候,嘿嘿,可能就不是那麼回事了~

不信你看,用javascript算的結果:

用python算的結果:

當然了,我嘗試着用其他語言來試一下,結果好像並不都是這樣。

其中,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.01012^2。所以,S爲0,M爲1.0101,E爲2。 而-5.25=-101.01=-1.01012^2.。所以S爲1,M爲1.0101,E爲2。

浮點數是如何存儲的,來看另一篇文章的簡單解說浮點數在內存中的存儲方式

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不會造成任何兩個浮點數的混淆。

好了,看完上面的浮點數的存儲原理後,是時候來解答,爲什麼計算機會算錯的問題了!

  • 遇到小數點後數字轉換爲實際存儲結構時,有的轉換是一個死循環,即不可能得到一個精確的值,而這個不精確的值再與其他數據做運算時,得到的結果自然也就可能存在差距了。至於有時候能得到準確的數值,有時候卻得不到準備的值,則是和逆轉換相關了(即內存結構轉換爲可視的十進制數據)!
  • 另一個存在誤差的原因,則是因爲在計算過程中進行了數據類型的轉換,因爲原數據本來就不是精確的值,所以在進行類型轉換後,就不會得到和原始值直接轉化的值的相同結果了。

所以,咱們在做需要高精度的計算場合時,使用計算機語言自帶的存儲結構可能會不滿足咱們的需求,當然這也很容易辦到,一般也會有第三方的解決方案,即換一種存儲結構就可能能解決這種問題了。

如 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

關於Fundebug

Fundebug專注於JavaScript、微信小程序、微信小遊戲、支付寶小程序、React Native、Node.js和Java實時BUG監控。 自從2016年雙十一正式上線,Fundebug累計處理了9億+錯誤事件,得到了Google、360、金山軟件、百姓網等衆多知名用戶的認可。歡迎免費試用!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章