JS魔法堂:完全理解0.1 + 0.2 === 0.30000000000000004的背後 基礎野:細說原碼、反碼和補碼 基礎野:細說無符號整數 基礎野:細說有符號整數 基礎野:細說浮點數

Brief                              javascript

  一天有個朋友問我「JS中計算0.7 * 180怎麼會等於125.99999999998,坑也太多了吧!」那時我猜想是二進制表示數值時發生round-off error所致使,但並不清楚具體是如何致使,而且有什麼方法去規避。因而用了3周時間靜下心把這個問題搞懂,在學習的過程當中還發現不只0.7 * 180==125.99999999998,還有如下的坑html

  1. 著名的 0.1 + 0.2 === 0.30000000000000004java

  2. 1000000000000000128 === 1000000000000000129git

 

IEEE 754 Floating-point                  github

  衆所周知JS僅有Number這個數值類型,而Number採用的時IEEE 754 64位雙精度浮點數編碼。而浮點數表示方式具備如下特色:post

  1. 浮點數可表示的值範圍比同等位數的整數表示方式的值範圍要大得多;學習

  2. 浮點數沒法精確表示其值範圍內的全部數值,而有符號和無符號整數則是精確表示其值範圍內的每一個數值;網站

  3. 浮點數只能精確表示m*2e的數值;編碼

  4. 當biased-exponent爲2e-1-1時,浮點數能精確表示該範圍內的各整數值;url

  5. 當biased-exponent不爲2e-1-1時,浮點數不能精確表示該範圍內的各整數值。

  因爲部分數值沒法精確表示(存儲),因而在運算統計後誤差會愈見明顯。

 

  想了解更多浮點數的知識可參考如下文章:

  基礎野:細說原碼、反碼和補碼(http://www.cnblogs.com/fsjohnhuang/p/5060242.html)

  基礎野:細說無符號整數(http://www.cnblogs.com/fsjohnhuang/p/5078290.html)

  基礎野:細說有符號整數(http://www.cnblogs.com/fsjohnhuang/p/5082829.html)

  基礎野:細說浮點數(http://www.cnblogs.com/fsjohnhuang/p/5109766.html)

 

Why 0.1 + 0.2 === 0.30000000000000004 ?       

  在浮點數運算中產生偏差值的示例中,最出名應該是0.1 + 0.2 === 0.30000000000000004了,到底有多有名?看看這個網站就知道了http://0.30000000000000004.com/。也就是說不只是JavaScript會產生這種問題,只要是採用IEEE 754 Floating-point的浮點數編碼方式來表示浮點數時,則會產生這類問題。下面咱們來分析整個運算過程。

  1. 0.1 的二進制表示爲 1.1001100110011001100110011001100110011001100110011001 1(0011)+ * 2^-4;

  2. 當64bit的存儲空間沒法存儲完整的無限循環小數,而IEEE 754 Floating-point採用round to nearest, tie to even的舍入模式,所以0.1實際存儲時的位模式是0-01111111011-1001100110011001100110011001100110011001100110011010;

  3. 0.2 的二進制表示爲 1.1001100110011001100110011001100110011001100110011001 1(0011)+ * 2^-3;

  4. 當64bit的存儲空間沒法存儲完整的無限循環小數,而IEEE 754 Floating-point採用round to nearest, tie to even的舍入模式,所以0.2實際存儲時的位模式是0-01111111100-1001100110011001100110011001100110011001100110011010;

  5. 實際存儲的位模式做爲操做數進行浮點數加法,獲得 0-01111111101-0011001100110011001100110011001100110011001100110100。轉換爲十進制即爲0.30000000000000004。

     

Why 0.7 * 180===125.99999999998 ?          

  1. 0.7實際存儲時的位模式是0-01111111110-0110011001100110011001100110011001100110011001100110;

  2. 180實際存儲時的位模式是0-10000000110-0110100000000000000000000000000000000000000000000000;

  3. 實際存儲的位模式做爲操做數進行浮點數乘法,獲得0-10000000101-1111011111111111111111111111111111111111101010000001。轉換爲十進制即爲125.99999999998。

 

Why 1000000000000000128 === 1000000000000000129 ?      

  1. 1000000000000000128實際存儲時的位模式是0-10000111010-1011110000010110110101100111010011101100100000000001;

  2. 1000000000000000129實際存儲時的位模式是0-10000111010-1011110000010110110101100111010011101100100000000001;

  3. 所以1000000000000000128和1000000000000000129的實際存儲的位模式是同樣的。

 

Solution                            

  到這裏咱們都理解只要採起IEEE 754 FP的浮點數編碼的語言均會出現上述問題,只是它們的標準類庫已經爲咱們提供瞭解決方案而已。而JS呢?顯然沒有。壞處天然是掉坑了,而好處偏偏也是掉坑了:)

  針對不一樣的應用需求,咱們有不一樣的實現方式。

  Solution 0x00 - Simple implementation

    對於小數和小整數的簡單運算可用以下方式

function numAdd(num1/*:String*/, num2/*:String*/) { 
    var baseNum, baseNum1, baseNum2; 
    try { 
        baseNum1 = num1.split(".")[1].length; 
    } catch (e) { 
        baseNum1 = 0; 
    } 
    try { 
        baseNum2 = num2.split(".")[1].length; 
    } catch (e) { 
        baseNum2 = 0;
    } 
    baseNum = Math.pow(10, Math.max(baseNum1, baseNum2)); 
    return (num1 * baseNum + num2 * baseNum) / baseNum; 
};

  Solution 0x01 - math.js

     若須要複雜且全面的運算功能那必須上math.js,其內部引用了decimal.jsfraction.js。功能異常強大,用於生產環境上妥妥的!

  Solution 0x02 - D.js

     D.js算是個人練手項目吧,截止本文發表時D.js版本爲V0.2.0,僅實現了加、減、乘和整除運算而已,bug是一堆堆的,但至少解決了0.1+0.2的問題了。

var sum = D.add(0.1, 0.2)
console.log(sum + '') // 0.3

var product = D.mul("1e-2", "2e-4")
console.log(product + '') // 0.000002

var quotient = D.div(-3, 2)
console.log(quotient + '') // -(1+1/2)

解題思路:

    1. 因爲僅位於Number.MIN_SAFE_INTEGER和Number.MAX_SAFE_INTEGER間的整數才能被精準地表示,也就是隻要保證運算過程的操做數和結果均落在這個閥值內,那麼運算結果就是精準無誤的;

    2. 問題的關鍵落在如何將小數和極大數轉換或拆分爲Number.MIN_SAFE_INTEGER至Number.MAX_SAFE_INTEGER閥值間的數了;

    3. 小數轉換爲整數,天然就是經過科學計數法表示,並經過右移小數點,減少冪的方式處理;(如0.000123 等價於 123 * 10-6)

    4. 而極大數則須要拆分,拆分的規則是多樣的。

        4.1. 按因式拆分:假設對12345進行拆分獲得 5 * 2469;

        4.2. 按位拆分:假設以3個數值爲一組對12345進行拆分獲得345和12,而實際值爲12*1000 + 345。

        就我而言,4.1的拆分規則結構不穩定,並且不直觀;而4.2的規則直觀,且拆分和恢復的公式固定。

    5. 餘數由符號位、分子和分母組成,而符號與整數部分一致,所以只需考慮如何表示分子和分母便可。

    6. 無限循環數則僅需考慮如何表示循環數段便可。(如10.2343434則分紅10.23 和循環數34和34的權重便可)

獲得編碼規則後,那就剩下基於指定編碼如何實現各類運算的問題了。

    1. 基於上述的數值編碼規則如何實現加、減運算呢?

    2. 基於上述的數值編碼規則如何實現乘、除運算呢?(其實只要加、減運算解決了,乘除必然可解,就是效率問題而已)

    3. 基於上述的數值編碼規則如何實現其它如sin、tan、%等數學運算呢?

另外因爲涉及數學運算,那麼將做爲add、sub、mul和div等入參的變量保持如同數學公式運算數般純淨(Persistent/Immutable Data Structure)是必須的,那是否還要引入immutable.js呢?(D.js如今採用按需生成副本的方式,可預見隨着代碼量的增長,這種方式會致使總體代碼沒法維護)

 

Conclusion                           

  依照個人尿性,D.js將採起不按期持續更新的策略(待我理解Persistent/Immutable Data Structure後吧:))。歡迎各位指教!

  尊重原創,轉載請註明來自:http://www.cnblogs.com/fsjohnhuang/p/5115672.html^_^肥子John

 

Thanks                              

http://es5.github.io

https://github.com/MikeMcl/decimal.js/

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

http://demon.tw/copy-paste/javascript-precision.html

相關文章
相關標籤/搜索