js浮點運算精度問題和IEEE754

原文鏈接

當我們使用一段時間的JS之後會遇到下面這個問題

  0.1 + 0.2 === 0.3 // false

我們可以在控制檯裏面看到0.1+0.2輸出的並不是0.3而是0.30000000000000004。那麼爲什麼出現這樣的問題呢。

其實出現這樣的問題在於js使用了IEEE754二進制浮點數算術標準,這個標準對於1/2, 1/4, 1/8等數字有很好的支持,其實這個標準也是現在大多數語言,CPU和浮點計算器選擇的浮點數算術標準。

因爲IEEE754是二進制的規則,所以可以看做用n*2^m這樣的表示方式,這樣的表達式在有限的存儲空間下無法表示n*10^m類型數字,所以很簡單的0.1無法被準確的表示。

但是類似Java等很多語言在底層會對浮點預算有處理,所以看上去好像沒有遇到JS中的問題

有人認爲, JavaScript 應該採用一種可以精確呈現數字的實現方式。 一直以 來出現過很多替代方案,只是都沒能成爲標準,以後大概也不會。這個問題 看似簡單,實則不然,否則早就解決了。

爲什麼說在有限的存儲空間下無法準確表示呢,那麼讓我們來了解一下IEEE754是如何來表示一個數字的吧。

關於IEEE754有單精度和雙精度兩種方式,兩個方式的計算規則都是一樣的,只是單精度使用32位來存儲一個數字,而雙精度使用64位來存儲,只是用於存儲的位數的大小。

關於IEEE754每位表示的意思,這裏不詳細說明,詳細可以查看wiki IEEE 754

簡單來說就是IEEE754對於數字的表達方式是

  n = (-1)^s * 2^(e-127) * (1 + f)

(右邊爲第0位)
單精度 s : 第31位
e : 第30至23位
f : 第22指0位

單精度 s : 第63位
e : 第62至52位
f : 第51指0位

我們舉一個例子來說明IEEE的表達方式

s e f
0 0111 1110 1100 0000 0000 0000 0000 000

在這裏s = 0,e轉爲10進制是126, f中左數第一位表示 1 / 2^1,第二位表示1 / 2^2,依次類推,所以在這裏f = 1 / 2^1 + 1 / 2^2 = 0.75;

所以 n = (-1)^0 * 2^(126-127) * (1+0.75) = 0.875;

倒過來,如果我們給到的數字是23.56,那麼首先我們先用二進制表示這個數字,爲10111.1000111101011100001,然後我們將小數點移到前面只有一位數字,這裏我們左移了4位
變成1.01111000111101011100001,然後除去第一位的01111000111101011100001填入22-0位,因爲是正數,所以第31位爲0,然後我們左移了4位,所以說明(e-127) = 4,所以e=131,轉爲二進制,所以第30至23位爲1000 0011,從而得到了結果。

下面我們來看一下ieee754中那個無法準確表達的0.1。
首先將0.1轉爲二進制,10進制轉二進制可以在網上查到,在如果不考慮存儲的話,應該是0.000110001100011…,可以看到是00011的無限循環,其實如果在存儲長度沒有限制的情況下,通過簡單的計算我們可以看到這個無限循環就是等於0.1。

000011 = 3 * 2 / (1 / 2^4), 等式爲 3* 2 ( 1/2^4 + 1/2^8 … 1/2^4n)。
通過等比數列運算 s = 3 * 2 * (1/2^4) * (1- (1/2^4)^n) / (1 - 1 /2^4) = 3 * 2 * 1 / 15 = 0.1

那麼我們有什麼辦法可以在js中安全的使用浮點運算呢,可能說沒有有完美的解決辦法

但是還是有一些簡單的處理辦法。

對於計算,我們可以使用toFixed來處理toFixed是用來強制保留小數點後面的位數,可以用於大多數精度要求不是非常高的計算中

(0.1 + 0.2).toFixed(2) // 0.30

最常見的方法是設置一個誤差範圍值, 通常稱爲“機器精度”(machine epsilon) , 對JavaScript的數字來說,這個值通常是 2^-52 (2.220446049250313e-16)

從 ES6 開始, 該值定義在 Number.EPSILON中, 我們可以直接拿來用, 也可以爲 ES6 之前 的版本寫 polyfill:

  if (!Number.EPSILON) {
    Number.EPSILON = Math.pow(2,-52);
  }

可以使用 Number.EPSILON來比較兩個數字是否相等(在指定的誤差範圍內):

  function numbersCloseEnoughToEqual(n1,n2) {
    return Math.abs( n1 - n2 ) < Number.EPSILON;
  }

  var a = 0.1 + 0.2;
  var b = 0.3;

  numbersCloseEnoughToEqual( a, b ); // true
  numbersCloseEnoughToEqual( 0.0000001, 0.0000002 ); // false

原文鏈接

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