js浮點數精度問題的前世今生?

1.該問題出現的原因 ?

2.為何其他編程語言,比如java中可能沒有chrome js console 中那麼明顯

3.大家在項目中踩過浮點數精度的坑?

4.最後採用哪些方案規避這個問題的?

5.為何採用改方案?

例如在 chrome js console 中:

alert(0.7+0.1); //輸出0.7999999999999999

相關問題:http://www.zhihu.com/question/20727110/answer/15978473


1. 主要問題是,你用十進位去想,0.7 是一個能準確表示的小數,而二進位卻是循環小數0.1dot{0}11dot{0}。反過來想,就好像在三進位中 0.2 是一個很準確的數字,但在十進位中卻是循環小數0.dot{6}。除非用有理數表示,這些數字不能精確地用有限位的二進位表示,產生誤差,0.7+0.1計算的結果也是有誤差。把有誤差的結果顯示時,轉換成十進位顯示的演算法發現該值與 0.8 相比,較接近 0.7999999999999999,所以顯示後者。

2. 一些語言的預設輸出數位是較小的,例如 C 的 printf("%f", x) 只顯示小數後6位,Java 的PrintStream.format("%f", x) 也是小數後 6 位 。而 javascript 預設是最精確的輸出,其他轉換方式例如有 Number.prototype.toPrecision()。

3, 4, 5. 問題太泛,恕不作答。


正好之前寫過兩篇文章:

  • 代碼之謎(四)- 浮點數(從驚訝到思考) - 代碼之謎 - 知乎專欄

  • 代碼之謎(五)- 浮點數(誰偷了你的精度?) - 代碼之謎 - 知乎專欄

-------------------------

1.該問題出現的原因 ?

浮點數 != 小數,之所以使用浮點數是在相同位數下對精度和範圍的取捨

2.為何其他編程語言,比如java中可能沒有chrome js console 中那麼明顯

對於浮點數的表示,幾乎所有語言都一樣,在我的文章「誰偷了你的精度」詳細說明了浮點數的精度在個環節丟失了,以及為什麼無法精確表示的小數卻能夠計算出精確的值。

3.大家在項目中踩過浮點數精度的坑?

---

4.最後採用哪些方案規避這個問題的?

涉及到精度要求高的場景,應該使用定點數表示小數。如 Java 中的 BigDecimal、C# 中的貨幣類型、MySQL 中的 NUMERIC 類型等。

5.為何採用改方案?

---


沒有浮點精度問題的語言……

我知道的就是 Coq,用的是戴德金分割


作者:陳嘉棟

鏈接:妥協與取捨,解構C#中的小數運算 - Runtime - 知乎專欄

來源:知乎

著作權歸作者所有。商業轉載請聯繫作者獲得授權,非商業轉載請註明出處。

0x00 前言

慕容在生活和工作中常常會遇到一些十分迷信機器的人,他們之中很多人都相信機器是最理智的,沒有任何感情,是真正的鐵面無私,因此機器的運算所給出
的答案總是正確的,如果答案錯誤,那麼一定是操作機器的人的問題。但機器的運算就一定是正確的嗎?事實上,機器出現運算錯誤並不是一個罕見的情況,一個典
型的例子便是小數運算。下面就讓我們來聊一個相關的話題,在機器或者具體的說在C#語言中小數是如何被處理的?

0x01 先從一個「錯誤」的答案說起

既然要聊一聊機器是怎麼把算術題做錯的,那麼我們自然要先來看一個機器運算錯誤的小例子。

#include &

void main(){
float num;
int i ;
num = 0;
for(i = 0; i &< 100; i++) { num += 0.1; } printf("%f ", num); }

這是一份C語言寫成的小程序,邏輯十分簡單易懂,所要實現的結果無非是將0.1相加100次之後再輸出。我想不需要計算機來計算,我們自己心算就能立刻得出答案——10。那麼計算機會交給我們一份什麼樣的答案呢?下面我們將這份C代碼編譯並且運行一下。

答案一輸出,就讓人大吃一驚。怎麼計算機還不如人的心算嗎?如果按照慕容在前言中提到的那些朋友這時可能就開始糾結是否是代碼寫錯了,亦或者是慕容
的電腦出現了什麼問題。但事實上代碼是正確的,機器也是運行如常的。那麼究竟是為什麼計算機的運算輸給了人的心算呢?這就引出了下一個問題,計算機是如何
處理小數的呢?如果我們了解一下計算機處理小數的機制,那麼這一切的迷就能夠解開了。(當然如果有朋友用C#來對0.1加100次,之後的結果是10。但
那是C#在幕後為我們做的一些隱藏工作,在計算機處理小數的問題上,本質是一樣的)。0x02 數字的格式

一個程序可以看做是現實世界的一個數字化的模型。現實世界中的一切都可以轉化為數字在計算機的世界中重新復活。因此,一個不得不解決的問題就是數字是如何在計算機中表達的。這也是數字格式出現的意義。

眾所周知,機器語言全部都是數字,但是本文自然不會關心全部的二進位格式。這裡我們只關心現實中有意義的數字是如何在計算機中被表示的。簡單而言,有意義的數字大體可以分為以下三種格式。

整數格式

我們在開發的過程中遇到的大部分的數字其實都是整數。而整數在計算機中也是最容易表示的一種。我們遇到的整數都可以使用32位有符號整數來表示(Int32)。當然,如果需要,還有有符號 64 位整數數據類型(Int64)可供選擇。至於和整數相對應的便是小數,而小數主要有兩種表示方式。

定點格式

所謂定點格式,即約定機器中所有數據的小數點位置是固定不變的。而定點小數的最常見的例子是SQL
Server中的money類型。事實上定點小數已經很不錯了,它顯然能夠適合很多需要處理小數的情況。但是它有一個與生俱來的缺點,那就是由於小數點的
位置固定,因此它能表示範圍是受限的。因此下面我們本文的主角就要出場了。

浮點格式

解決定點格式先天問題的方案便是浮點格式的出現。而浮點格式的組成則包括符號、尾數、基數和指數,通過這四部分來表示一個小數。由於計算機內部是二
進位的,因此基數自然而然是2(就如十進位的基數是10一樣)。因此計算機在數據中往往無需記錄基數(因為總是2),而是只用符號、尾數、指數這三部分來
表示。很多編程語言都至少提供了兩種使用浮點格式表示小數的數據類型,即我們常常能見到的雙精度浮點數double和單精度浮點數float。同樣,在我
們的C#語言中也存在著這兩種使用了浮點格式表示小數的數據類型——按照C#語言標準雙精度浮點數和單精度浮點數在C#中對應的是
System.Double和System.Single。但是事實上在C#語言中還存在著第三種使用了浮點格式表示小數的數據類型,那就是
decimal類型——System.Decimal。需要注意的是,浮點格式的表示形式有很多,而在C#中遵循的是IEEE 754標準:

  • float單精度浮點數為32位。32位的構造為:符號部分1bit、指數部分8bit以及尾數部分23bit。
  • double雙精度浮點數為64位。64位的構造為:符號部分1bit、指數部分11bit以及尾數部分52bit。

0x03 表示範圍、精度和準確度

既然聊完了數字在計算機中的幾種表示形式,那麼接下來我們就不得不提一下在選擇數字格式時的一些指標。最常見的無非是這幾點:表示範圍、精度、準確度。

數字格式的表示範圍

顧名思義,數字格式的表示範圍指的就是這種數字格式所能表示的最小的值最大的值範圍
例如一個16位有符號整數的表示範圍是從-32768到32767。如果要被表示的數字的值超出了這個範圍,那麼使用這種數字格式就不能正確的表示這個數
字了。當然在這個範圍內的數字也有可能無法被正確的表示,例如16位有符號整數是無法準確表示一個小數的,但是總有一個接近的值是可以用16為有符號整數
格式來表示的。

數字格式的精度

實話實說,精度和準確度讓很多人都有一種十分模糊的感覺,似乎是一樣的卻又有區別。但慕容需要提醒各位注意的是,精度和準確度是兩個有巨大差距的概念。

通俗的來講,數字格式的精度可以認為是該格式有多少信息用來表示一個數字。更高的精度通常意味著能夠表示更多的數字,一個最明顯的例子便是精度越高
那麼這種格式所能表示的數字就越接近真實的數字。例如我們知道1/3如果換算成小數0.3333....是無窮盡的,那麼它在五位精度的情況下可以寫成
0.3333,而在七位的情況下就又變成了0.333333(當然,如果用七位表示五位,那麼就是0.333300)。

數字格式的精度還會影響到計算的過程。舉一個簡單的例子,如果在計算中我們使用的是一位精度。那麼整個計算可能就變成了下面的這種情況:

0.5 * 0.5 + 0.5 * 0.5 = 0.25 + 0.25

= 0.2 + 0.2

=0.4

而如果我們使用的是兩位精度,那麼計算過程又會變成下面的情況。

0.5 * 0.5 + 0.5*0.5 = 0.25 + 0.25

           =0.5

對比兩種精度情況下的計算結果,一位精度情況下的計算結果和正確的結果差了0.1。而使用了兩位精度的情況則正常的計算出了結果。因此可以發現在計算的過程中保證精度是一件多麼有意義的事情。

數字格式的準確度

數字格式的表示範圍、精度都已經介紹完了,那麼接下來我們就來介紹一下數字格式的準確度。剛剛已經說過了,準確度和精度是一對經常讓人混淆的概念。

那麼我們再通俗的給準確度來個注釋,簡單的說它表示的是該數字格式(特定環境)所表示的數字和真實數字的誤差。準確度越高,則意味著數字格式所表示的數字和真實數字的值之間的誤差越小。準確度越低,則意味著數字格式所表示的數字和真實數字的值之間的誤差越大。

需要注意的一點是數字格式的精度和數字格式的準確度並沒有直接的關係,這一點也是很多朋友在概念上常常會混淆的地方。使用低精度的數字格式表示的數字,並不一定要比使用高精度的數字格式所表示的數字的準確度低。

舉一個簡單的例子:

Byte num = 0x05;
Int16 num1 = 0x0005;
Int32 num2 = 0x00000005;
Single num3 = 5.000000f;
Double num4 = 5.000000000000000;

此時,我們分別使用5種不同的數字格式表示同一個數字5,雖然數字格式的精度(從8位到64位)不同,但是通過數字格式所表示出來的數和真實的數是一樣的。也就是說對於數字5,這5種數字格式的準確度相同。

0x04 取整誤差

了解了計算機中常見的幾種數字格式之後,現在我們再來聊一聊計算機是如何通過數字格式來表示現實世界中的數字的。眾所周知,計算機中使用的是0和
1,即二進位,使用二進位表示整數是十分容易的一件事情,不過在使用二進位表示小數時,我們往往會產生一些疑問。例如二進位小數1110.1101換算成
十進位是多少呢?第一眼看上去多了一個小數點,似乎讓人十分困惑。事實上它的處理和整數是一樣的,即將各個數位的數值和位權相乘結果求和。小數點前的位
權,大家都已經十分熟悉了,從右向左分別是0次冪、1次冪、2次冪以此遞增,因此小數點前的二進位換算為十進位便是:

1 * 8 + 1 * 4 + 1 * 2 + 0 = 14

而在小數點之後的位權,相應的從左向右分別是-1次冪、-2次冪依次遞減。因此小數點之後的二進位轉換為十進位便是:

1 * 0.5 + 1 * 0.25 + 0 * 0.125 + 1 * 0.0625 = 0.8125

因此1110.1101這個二進位小數轉換為十進位便是14.8125。

通過觀察小數點之後的二進位轉換為十進位的過程,各位看官是否發現了很有趣的一個事實呢?那就是小數點之後的二進位並不能表示所有的十進位數,換言
之有一些十進位數是無法轉換成二進位的。這個很好理解,因為小數點之後,二進位的位權按照除以2的節奏遞減,而十進位卻是按照除以10的節奏遞減。因此如
果小數點後4位用二進位表示,即從.0000~.1111這個範圍內連續的二進位數值事實上對應的十進位數是不連續的,所有可能的結果也不過是各個位權
(0.5、0.25、0.125以及0.0625)相加的組合而已。

因此,一個在十進位中十分簡單的數字如果用二進位來準確無誤的表示,所使用的位數可能會十分長甚至是無限的。一個很好的例子便是使用二進位浮點數來表示十進位中的0.1:

double x = 0.1d;

事實上變數x中所保存的值並不是真正的十進位中的0.1,而是一個最接近十進位0.1的二進位浮點數。這是因為無論小數點之後有多少位二進位的數,2的負數次冪都無法相加得到0.1這個結果,因此0.1這個十進位數在二進位中會變成一個無限小數。

當然二進位有可能無法準確的表示一個十進位小數很好理解,因為這有點類似於在十進位中我們同樣無法準確表示1/3這樣的循環小數。

此時,我們便不得不和計算機妥協了。因為我們現在知道了在計算機中使用的數值可能並不等於真實世界中的數值,而是計算機使用某種數字格式表示的一個
十分接近原始數字的一個值。而在整個程序運行的過程中,我們的計算機就要一直使用這個僅僅是近似的數值來參與計算,我們假設真實的數值是n,而計算機事實
上會使用另一個數值n + e(當然e是一個可正可負且十分小的數)來參與計算機中的運算。此時,這個數值e便是取整誤差。

而這還僅僅是一個數字在計算機中使用近似值來表示,如果該數值參與到計算中去,那麼顯然會帶來更多誤差。這也是本文一開始那個c程序之所以計算錯誤
的原因,因為無法正確的表示參與計算的值,到最後都變成了近似值。當然C#語言相對而言要「高級」了很多,雖然在計算機中也是近似值,但是展示在大家眼前
的至少還是更加符合人們「預期」的值。不過在C#中,小數計算真的是不會出錯的嗎?畢竟,這一切似乎僅僅是障眼法。

0x05 取與舍,C#的小數比比是否相等

不知道各位看官在使用一些關係運算符時,有沒有留意到直接使用等號比較兩個小數是否相等時是否會出現一些意想不到的問題。我身邊的朋友使用關係運算
符直接比較兩個小數大小的情況比較多,而直接比較兩個小數是否相等的情況卻不太多。同時我在此也想提醒各位最好不要輕易比較兩個小數是否相等,即便在C#
這種高級語言中仍然可能得到讓人感覺「錯誤」的答案,這是因為我們事實上比較的是兩個小數是否「接近」於相等,而不是兩個數是否是真正的相等。下面這個例
子可能會更好的說明這一點:

using System;

class Test
{
static void Main()
{
double f = Sum (0.1d, 0.2d);
double g = 0.3d;
Console.WriteLine (f);
Console.WriteLine (f==g);
}

static double Sum (double f1, double f2)
{
return f1+f2;
}
}

我們編譯並且運行這段代碼,可以看到輸出了如下的內容:

比較這兩個小數的結果並不是true,這和我們的預期並不一樣。浮點數的真模樣

我們知道,像上文中的那個二進位小數1110.1101事實上也是按照人類習慣表達出來的,但是計算機可是不能識別這種帶小數點的東西的哦。所以計
算機會使用之前介紹的數字格式來表示這樣一個數字,那麼一個二進位浮點數在計算機中到底是如何表現的呢?其實在上文介紹數字格式的部分已經介紹過了,但是
沒有實際看一眼終究是不能有一個直觀的認識,那在本文的最後,我們就來看一個二進位浮點數的在計算機中真實的樣子。

0100000001000111101101101101001001001000010101110011000100100011

這是一個64位的二進位數。如果把它作為一個雙精度浮點數,那麼它的各部分都分別表示了什麼呢?

按照上文介紹浮點數的部分,我們可以將它分成如下幾部分:

符號:0

指數部分:10000000100(二進位,可以轉換為十進位的1028)

尾數部分:0111101101101101001001001000010101110011000100100011

因此,將它轉換為一個用二進位表示的小數,則是:

(-1)^0 * 1.0111101101101101001001001000010101110011000100100011 x 2^(1028-1023)

= 1.0111101101101101001001001000010101110011000100100011 x 2^5

= 101111.01101101101001001001000010101110011000100100011

如果各位讀者觀察足夠仔細的話,是否發現了有趣的一點呢?那就是在這個在計算機中用來表示雙精度浮點數的64位數中,尾數部分的幾位數字是:0111101101101101001001001000010101110011000100100011

但是經過從計算機中的形式轉化成人類使用二進位表示小數的形式之後,數字卻變成了1.0111101101101101001001001000010101110011000100100011x 2^5,小數點之前為什麼會多出了一個1呢?

這是因為在尾數部分,為了將表現形式多樣的浮點數統一為同一種表示方式而規定要將小數點前的值固定為1。由於小數點前的數永遠是1,因此為了節省一個數據位,這個1在計算機中並不需要被保存。

那麼應該如何保證一個二進位小數的小數點前的值是1呢?這就需要對二進位小數進行邏輯移位了,通過左移或右移若干次後,將整數部分變為1。例如上文中的這個二進位小數:1110.1101,我們就來試試如何把它變成計算機可以識別的浮點數的尾數吧。

1110.1101(原始數據)——&>0001.1101101(通過右移將整數部分變為1)——
&>0001.11011010000000000000....(拓展位數,使之符合數字格式的規定)——
&>11011010000000000000....(去掉整數部分,僅保留小數部分)

好了,到此關於C#或者計算機中的小數計算就寫得差不多了。歡迎各位交流。


不光是js,只要採用IEEE754浮點數標準的語言都存在這個問題。IEEE754浮點數主要有單精度(32位)和雙精度(64位),js採用雙精度。有些浮點數比如0.1轉化為二進位是無窮的,而64位的浮點數表示法尾數位只允許52位,超出的部分進一舍零,會造成浮點數精度丟失,兩個浮點數轉化二進位相加後的結果,也遵循這個原則。


不存在你說的情況,只有……計算機有……

看你不是計算機專業……然後,有不去讀計算機方面的書——雖然我也是半吊子……哈哈

……

大部分的語言,浮點數,都才有 什麼IEEE- 7几几標準去了…… 的 浮點數標準……

……可以看下,數的起源……

小數,都是幾分之一,幾分之一…………

計算機 1/2 1/4 1/8

然後……

解決問題……

就是 ,需要保留最小位數,……*10^n 再 /10^n……

問題做了些補充:

http://www.zhoulujun.cn/zhoulujun/html/theory/computBase/2016_0714_7860.html


自然數表達不了所有整數,整數表達不了所有有理數,有理數表達不了所有實數,

更複雜的運算往往會拓展數的範圍,更簡單的表示形式只能近似,而不能精確表達.

所以要解決這個問題那麼

1.如果是允許誤差的計算,增加精度,減少會導致誤差的運算次數(就是採用數值穩定性好的演算法).

2.不允許誤差的計算,採用該種運算的規則和表示法,比如:用兩個整數表示分數,兩個整數表示小數(用一個整數表示小數點偏移).按規則定義的方法進位,等等.


針對你給的例子,你可以將兩個數都10然後再除以一百試試,計算機在將一個10進位有限小數轉換為2進位的時候不一定是有限的,結果就會造成偏差


推薦閱讀:

為什麼JS中一個浮點數位或0會去掉小數部分?
怎樣精確區分這些名詞:庫、插件、組件、控制項、擴展?
sass、scss、less、compass、bootstrap學習的順序是什麼?
沒有Fork而是直接拷貝代碼到自己的倉庫並進行修改增加新的功能,是否算抄襲?
Sublime Text 3中如何一次性修改變數及其引用?

TAG:JavaScript | 浮點運算 |