天天看點

0.1 + 0.2 === 0.3 ?小小的腦袋充滿大大的疑惑什麼是IEEE 754?0.1 + 0.2 !== 0.3相關知識點如何解決

小小的腦袋充滿大大的疑惑

0.1 + 0.2 === 0.3 ?小小的腦袋充滿大大的疑惑什麼是IEEE 754?0.1 + 0.2 !== 0.3相關知識點如何解決

關于

0.1 + 0.2 === 0.3

,大家應該都知道這個結果是false。

0.2 + 0.3 === 0.5

得到的又是true,這到底是咋肥事?

到底哪些計算是成立的?哪些又是不成立的?為什麼?如何解決?

帶着這些問題開始我們的探究

// 成立的
0.1 + 0.3 === 0.4 // true
0.2 + 0.2 === 0.4 // true
// 不成立的
0.1 * 3 === 0.3 // false
0.2 * 6 === 1.2 // false      

什麼是IEEE 754?

首先我們要知道,無論是整數還是浮點數,在cpu中都是通過轉為二進制計算的,整數轉為二進制還是很簡單的。那麼,如何将浮點數轉為二進制數?此時我們就要引入一個關鍵字:IEEE754(二進制浮點數算術标準)

關于什麼是IEEE 754,維基百科中的是這麼解釋的:

IEEE二進制浮點數算術标準(IEEE 754)是20世紀80年代以來最廣泛使用的浮點數運算标準,為許多CPU與浮點運算器所采用。

大部分程式設計語言都提供了IEEE浮點數格式與算術,但有些将其列為非必需的。

有此可知,不僅是JavaScript,隻要是使用了IEEE 754浮點數格式,來存儲浮點類型(float 32,double 64)的任何程式設計語言都有這個問題!下面是我用C++舉的例子:

0.1 + 0.2 === 0.3 ?小小的腦袋充滿大大的疑惑什麼是IEEE 754?0.1 + 0.2 !== 0.3相關知識點如何解決

知道了前因,下面我來看一下到底如何把浮點數轉為二進制!!!

0.1 + 0.2 === 0.3 ?小小的腦袋充滿大大的疑惑什麼是IEEE 754?0.1 + 0.2 !== 0.3相關知識點如何解決

浮點數轉為二進制

首先,JavaScript中的Number是采用64位存儲的,這64位由3部分組成,(S:符号位,Exponent:指數域,Fraction:尾數域)。

如下圖(從右到左):

0.1 + 0.2 === 0.3 ?小小的腦袋充滿大大的疑惑什麼是IEEE 754?0.1 + 0.2 !== 0.3相關知識點如何解決

綜合科學計數法,可以通過以下的計算公式表示:

(-1)s * 1.F * 2E-1023

其中S表示符号,0表示正數、1表示負數

1023為偏移量,32位單精度偏移量位127,下圖中的S、P、M隻是叫法不同,本文以維基百科中單詞(S、F、E)為準。

0.1 + 0.2 === 0.3 ?小小的腦袋充滿大大的疑惑什麼是IEEE 754?0.1 + 0.2 !== 0.3相關知識點如何解決

那麼S、F、E如何獲得呢?

結算過程

拿263.3舉例:

1.首先先擷取263的二進制,如下圖

0.1 + 0.2 === 0.3 ?小小的腦袋充滿大大的疑惑什麼是IEEE 754?0.1 + 0.2 !== 0.3相關知識點如何解決

可得出263的二進制為:100000111

2.再計算0.3的二進制,如下圖

0.1 + 0.2 === 0.3 ?小小的腦袋充滿大大的疑惑什麼是IEEE 754?0.1 + 0.2 !== 0.3相關知識點如何解決

根據規律我們可以看出1001是循環部分,是以得出0.3的二進制為:01001100110011001............

是以263.3的二進制表示為100000111.01001100110011001............

3. 計算S

由于263.3是正數,所有用0表示,故目前64位展示位

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

4. 計算E

由于要計算為1.F * 2n的形式,是以要把100000111.01001100110011001............ 小數點移到前面,成為1.0000011101001100110011001............

小數點移動了8位,即28

由公式E-1023=8,得出E為1031,我們将1031(1024+7)轉為二進制為:

10000000111

0.1 + 0.2 === 0.3 ?小小的腦袋充滿大大的疑惑什麼是IEEE 754?0.1 + 0.2 !== 0.3相關知識點如何解決

目前結果:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64
1 1 1 1 _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

4. 計算F

目前計算的值為:1.0000011101001100110011001............

将最高位去掉,剩下的填滿52位即可

即F為:0000011101001100110011001100110011001100110011001100

因第53位為1,二進制中的四舍五入(零舍一入),故最終結果如下:

0000011101001100110011001100110011001100110011001101

5.最終結果

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64
1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1

大家也可通過通過這個網站直接計算出SEM的值來做驗證。

0.1 + 0.2 !== 0.3

回到問題本身,我們可以通過計算0.1、0,2的二進制,經過相加計算就可以知道為什麼等式不成立了。

0.1的二進制

通過在 https://babbage.cs.qc.cuny.edu/IEEE-754.old/Decimal.html 中計算得出

2-4*1 .1001100110011001100110011001100110011001100110011010

0.2的二進制

2-3*1 .1001100110011001100110011001100110011001100110011010

計算0.1 + 0.2的二進制

得到值之後該怎麼處理呢?浮點數的加法是怎麼進行的?那就是先對齊、再計算。

對齊以高數位為準,我們此處的例子中-3為高位。是以需要将0.1的二進制處理下,向左移動一位。

移動前:

2-4*1 .1001100110011001100110011001100110011001100110011010

移動後:

2-3*0 .1100110011001100110011001100110011001100110011001101

然後我們計算兩個二進制數:

0 .1100110011001100110011001100110011001100110011001101 +

1 .1001100110011001100110011001100110011001100110011010 =

10.0110011001100110011001100110011001100110011001100111

因為我們計算的數的格式為1.xxx,是以小數點要前移一位(此時指數由-3變成了-2),但這樣尾數就53位了,根據尾數後一位1進0舍的規則,

移動前:

10.0110011001100110011001100110011001100110011001100111

移動後:

1.0011001100110011001100110011001100110011001100110100

将指數去除,最後的小數位值為:

0100110011001100110011001100110011001100110011001101

根據二進制計算規則,小數點後第一位為0.5,第二位為0.25,第三位為0.125....

由于位數太多,我們寫個簡單的計算函數幫助我們計算:

// 二進制小數
    var binaryDecimal = '0100110011001100110011001100110011001100110011001101';
    // 二進制小數點後對位值
    var counterpointArr = [];
    // 小數點後二進制的第一個對位值
    var counterpoint = 0.5;
    for (var i = 0; i < 52; i++) {
        counterpointArr.push(counterpoint);
        counterpoint = counterpoint / 2;
    }
    console.log(counterpointArr) // [0.5, 0.25, 0.125, 0.0625, 0.03125, 0.015625, 0.0078...]
    let sum = 0;
    for (var j = 0; j < binaryDecimal.length; j++) {
        if (binaryDecimal.charAt(j) === '1') {
            sum = sum + counterpointArr[j]
        }
    }
    console.log(sum) // 0.30000000000000004      
0.1 + 0.2 === 0.3 ?小小的腦袋充滿大大的疑惑什麼是IEEE 754?0.1 + 0.2 !== 0.3相關知識點如何解決

相關知識點

特殊場景

0.1 + 0.2 === 0.3 ?小小的腦袋充滿大大的疑惑什麼是IEEE 754?0.1 + 0.2 !== 0.3相關知識點如何解決

如何解決

簡單的普通場景

思路1

通過內插補點跟最小精度比較。

Number.EPSILON是 JavaScript 能夠表示的最小精度。誤差如果小于這個值,就可以認為已經沒有意義了,即不存在誤差了。

function epsEqu(x, y) {
    return Math.abs(x - y) < Number.EPSILON; //  [ˈepsɪlɒn]
}
console.log(epsEqu(0.1+0.2, 0.3));   // true
console.log(epsEqu(268.34+0.85, 269.19));  // true      

思路2

思路就是将小數轉為整數,然後再轉為小數表示。

//注意要傳入兩個小數的字元串表示,不然在小數轉成二進制浮點數的過程中精度就已經損失了
function numAdd(num1/*:String*/, num2/*:String*/) { 
    var baseNum, baseNum1, baseNum2; 
    try { 
        //取得第一個操作數小數點後有幾位數字,注意這裡的num1是字元串形式的
        baseNum1 = num1.split(".")[1].length; 
    } catch (e) {
        //沒有小數點就設為0 
        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; 
};
console.log(numAdd('0.1', '0.2')) // 0.3
console.log(numAdd('268.34', '0.85')) // 269.18999999999994
// 其實還是因為num1 * baseNum 導緻的,baseNum為浮點型,先從十進制轉為二進制,二進制時丢失了精度
// 解決方法是 return修改為return (num1 * baseNum + num2 * baseNum).toFixed(0) / baseNum;
// 因為我們已經確定了隻需要整數位      

上面的代碼還是比較簡單的,但針對大數、特殊場景都沒做處理,我們可以使用下面的庫

常見庫

1.Math.js

專門為 JavaScript 和 Node.js 提供的一個廣泛的數學庫。支援數字,大數字(超出安全數的數字),複數,分數,機關和矩陣。 功能強大,易于使用。

官網:mathjs.org/

GitHub:github.com/josdejong/m…

2.big.js

官網:mikemcl.github.io/big.js

GitHub:github.com/MikeMcl/big…

扔給後端【推薦】

嘿嘿,後端用的也不是float、double,而是用的bigdecimal。

ps.目前組内針對數值計算一律交給後端的。

0.1 + 0.2 === 0.3 ?小小的腦袋充滿大大的疑惑什麼是IEEE 754?0.1 + 0.2 !== 0.3相關知識點如何解決

感謝那些對我有幫助的文檔:

https://juejin.cn/post/6844903680362151950

https://babbage.cs.qc.cuny.edu/IEEE-754.old/Decimal.html

https://www.bilibili.com/video/BV1i54y1y7Fn?from=search&seid=16329325986965638794

https://www.bilibili.com/read/cv6776011/

https://segmentfault.com/a/1190000009084877

繼續閱讀