一区二区三区日韩精品-日韩经典一区二区三区-五月激情综合丁香婷婷-欧美精品中文字幕专区

分享

浮點數(shù)計算異常原因(轉)

 hh3755 2014-09-10

光棍節(jié)加長版

轉:http:///codepuzzle/2012/11/11/codepuzzle-float-who-stole-your-accuracy.html

代碼之謎(五)- 浮點數(shù)(誰偷了你的精度?)

如果我告訴你,中關村配置最高的電子計算機的計算精度還不如一個便利店賣的手持計算器,你一定會反駁我:「今天寫博客之前又忘記吃藥了吧」。

你可以用最主流的編程語言計算 0.2 + 0.4,如果你使用的是 Chrome、FireFox、IE 8+,可以按 F12 鍵,然后找到 「控制臺」,輸入上面的 表達式 0.2 + 0.4,回車。

然后再用最簡陋的計算器(如果你沒有手持計算器沒關系,手機、電腦都自帶一個計算器,打開“運行”,輸入 calc,回車) 再計算一下剛才的 算式 0.2 + 0.4。

怎么樣?同意我的觀點了吧! 再簡陋的計算器也比超級計算器的精度高,關鍵不在于它的頻率和內存,而在于它是如何設計、如何表示、如何計算的。

不能表示 VS 不能精確表示

在上一章『浮點數(shù)(從驚訝到思考)』中我們講到用浮點數(shù)表示 數(shù) 時出現(xiàn)的問題——很多數(shù)都 不能表示。(注意 浮點數(shù)表示的是數(shù),而不僅僅是小數(shù)。)

如果你數(shù)學比較好,或者你確信你身體健康,沒有心臟病、高血壓,沒有受過重大精神創(chuàng)傷,那我告訴你, 在浮點數(shù)的表示范圍內,有多于 99.999...% 的數(shù)在計算機中是 不能表示 的。 真的是太令人吃驚,也太令人遺憾了。 真相總是很殘忍。

請注意我使用的措辭,區(qū)別開 不能表示不能精確表示。

下面我從數(shù)量級分析一下,32bit 浮點數(shù)的表示范圍是 10 的 38 次方,而表示個數(shù)呢,是 10 的 10 次方。 能夠被表示的數(shù)只有 1/100000000.... (大概有30個零),這個數(shù)多大呢?還記得那個國際象棋和麥子的故事嗎?

為了讓你了解 指數(shù)的威力,我再舉個例子:

有一張很大很大的紙,對折 38 次,會有多高呢? 一米?一百米?比珠峰還高?再次考驗你心臟承受能力的時刻到了:它不僅僅比珠峰高,其實它已經(jīng)快到達月球了。

回到原來的話題,還有更殘忍的真相。 在剩下的可以表示的不到 0.000...1% 的數(shù)中,又有多少不能精確表示呢?這就是我寫這篇博客的目的。

上一章中我還給出了一種用定點數(shù)精確表示小數(shù)的方法。 事實上,手持計算器、java 中的 BigDecimal、C# 中的貨幣類型、MySQL 中的 NUMERIC 類型就是這么干的。 你還記得在數(shù)據(jù)庫中添加字段時的 SQL 語句是如何寫的嗎?現(xiàn)在明白為什么我說 再簡陋的計算器也比超級計算器的精度高 了吧。

這篇博客我將為大家講解為什么很多數(shù) 不能精確表示,本篇可能比較燒腦子,我會盡量用最通俗的語言,最貼近現(xiàn)實的例子來講解,不在乎篇幅有多長,關鍵是要給大家講明白。下一篇,你將了解到浮點數(shù)如何工作,以及為什么很多數(shù) 不能表示。

熱身 —— 問:要把小數(shù)裝入計算機,總共分幾步?你猜對了,3 步。

  • 第一步:轉換成二進制
  • 第二步:用二進制科學計算法表示
  • 第三步:表示成 IEEE 754 形式

在上面的第一步和第三步都有可能 丟失精度

十進制 VS 二進制

下面我們討論如何把十進制小數(shù)轉換成二進制小數(shù)(什么?你不會?請自覺去面壁)。

考慮我們將 1/7(七分之一) 寫成小數(shù)的時候是如何做的?

用 1 除以 7,得到的商就是小數(shù)部分,剩下的余數(shù)我們繼續(xù)除以 7,一直除到什么時候結束呢? 有兩種情況:

  1. 如果余數(shù)為 0。yeah!終于結束了,洗洗睡吧

  2. 當除到某一步時,余數(shù)等于 1… 停!stop!等一下,我發(fā)現(xiàn)有什么地方怪怪的。余數(shù)為 1,余數(shù)如果為 1 的話,再繼續(xù)除下去,不就又是 1/7 了嗎?繞了一個大彎,又回來了?對,你猜的很對,它永遠不會結束,它循環(huán)了。

注意我上面說的 情況2,我們判斷他循環(huán),并 不是從直觀看感覺它重復了,而是因為 在計算過程中,它又回到了開頭**。為什么這么說呢?當你計算一個分數(shù)時,它總是連續(xù)出現(xiàn) 5,出現(xiàn)了好多次,例如 0.5555555… 你也無法斷定它是無限循環(huán)的,比如 一億分之五。

記得高中時,從一本數(shù)學課外書學到了手動開平方的方法,于是很興奮的去計算 2 的平方根,發(fā)現(xiàn)它的前幾位是 1.414,哇,原來「2的平方根」等于 1.414141…。很多天以后,當我再次看到我的筆記時,只能苦笑了,「2的平方根」不可能循環(huán)啊,它可是一個無理數(shù)啊。

你可能不耐煩了,嘰哩哇啦說這么多,有用嗎?當然有用了,以后如果 MM 問你:你會愛我到什么時候?你可以回答她:我會愛你到 1/7 的盡頭。難道我會把我的表白方式告訴你們嗎? 我對你的愛就像圓周率,無限——卻永不重復。

扯遠了,現(xiàn)在會到主題。 你也許會說:我明白了,循環(huán)小數(shù)不能精確表示,放到計算機中會丟失精度; 那么有限小數(shù)可以精確表示吧,比如 0.1。

對于無限小數(shù),不只是計算機不能精確表示,即使你用別的辦法(省略號除外),比如紙、黑板、寫字板…都無法精確表示。什么?手機?也不能,當然不能了。不,不,iPad也不行,1萬買的也不行,真的,再貴的本子也寫不下。

哪些數(shù)能精確表示?

那么 0.1 在計算機中可以精確表示嗎?

答案是出人意料的, 不能

在此之前,先思考個問題: 在 0.1 到 0.9 的 9 個小數(shù)中,有多少可以用二進制精確表示呢?

我們按照乘以 2 取整數(shù)位的方法,把 0.1 表示為二進制(我假設那些不會進制轉換的同學已經(jīng)補習完了):

(1) 0.1 x 2 = 0.2  取整數(shù)位 0 得 0.0
(2) 0.2 x 2 = 0.4  取整數(shù)位 0 得 0.00
(3) 0.4 x 2 = 0.8  取整數(shù)位 0 得 0.000
(4) 0.8 x 2 = 1.6  取整數(shù)位 1 得 0.0001
(5) 0.6 x 2 = 0.2  取整數(shù)位 1 得 0.00011
(6) 0.2 x 2 = 0.4  取整數(shù)位 0 得 0.000110
(7) 0.4 x 2 = 0.8  取整數(shù)位 0 得 0.0001100
(8) 0.8 x 2 = 1.6  取整數(shù)位 1 得 0.00011001
(9) 0.6 x 2 = 1.2  取整數(shù)位 1 得 0.000110011
(n) ...

我們得到一個無限循環(huán)的二進制小數(shù) 0.000110011...

我為什么要把這個計算過程這么詳細的寫出來呢?就是為了讓你看,多看幾遍,再多看幾遍,繼續(xù)看… 還沒看出來,好吧,把眼睛揉一下,我提示你,把第一行去掉,從 (2) 開始看,看到 (6),對比一下 (2) 和 (6)。 然后把前兩行去掉,從 (3) 開始看…

明白了吧,0.2、0.4、0.6、0.8 都不能精確的表示為二進制小數(shù)。 難以置信,這可是所有的偶數(shù)啊!那奇數(shù)呢? 答案就是:

0.1 到 0.9 的 9 個小數(shù)中,只有 0.5 可以用二進制精確的表示。

如果把 0.0 再算上,那么就有兩個數(shù)可以精確表示,一個奇數(shù) 0.5,一個偶數(shù) 0.0。 為什么是兩個呢?因為計算機二唄,其實計算機還真夠二的。

世界上有 10 種人,一種是懂二進制的,一種是不懂二進制的。

其實答案很顯然,我再領大家換個角度思考,0.5 就是一半的意思。 在十進制中,進制的基數(shù)是 10,而 5 正好是 10 的一半。 2 的一半是多少?當然是 1 了。 所以,十進制的 0.5 就是二進制的 0.1。如果我用八進制呢? 不用計算你就應該立刻回答:0.4;轉換成十六進制呢,當然就是 0.8 了。

(0.5)10 = (0.1)2 = (0.4)8 = (0.8)16

如果你還想繼續(xù)思考,就又會發(fā)現(xiàn)一個有趣的事實,我們稱之為 定理A。 我們上面的數(shù),都是小數(shù)點后面一位小數(shù),因此,在十進制中,這樣的小數(shù)有 10 個(就是 0 到 9); 同理,在二進制中,如果我們讓小數(shù)點后面有一位小數(shù),應該有多少個呢?當然是 2 個了(0 和 1)。

哇,好像發(fā)現(xiàn)了新大陸一樣,很興奮是吧。那我再給你一棒,其實定理A是錯的。再重申一遍 盡信書,則不如無書。我寫博客的目的 不是把我的思想灌輸?shù)侥愕哪X子里,你應該有自己的思想,自己的思考方式,當我得出這個結論時,你應該立刻反駁我:“按照你的思路,如果是 16 進制的話,應該可以精確表示所有的 0.1 到 0.9 的數(shù)甚至還可以精確表示其它的 6 個數(shù)。而事實呢,16 進制可以精確表示的數(shù) 和 2 進制可以精確表示的數(shù)是一樣的,只能精確表示 0.5。”

那么到底怎么確定一個數(shù)能否精確表示呢?還是回到我們熟悉的十進制分數(shù)。

1/2、5/9、34/25 哪些可以寫成有限小數(shù)?把一個分數(shù)化到最簡(分子分母無公約數(shù)),如果分母的因式分解只有 2 和 5,那么就可以寫成有限小數(shù),否則就是無限循環(huán)小數(shù)。為什么是 2 和 5 呢?因為他們是 10 的因子 10 = 2 x 5。

二進制和十六進制呢?他們的因子只有 2,所以十六進制只是二進制的一種簡寫形式,它的精度和二進制一樣。

如果一個十進制數(shù)可以用二進制精確表示,那么它的最后一位肯定是 5。

備注:這是個必要條件,而不是充分條件。一位熱心網(wǎng)友設計出了下面的解決精度的方案。我就不解釋了,同學們自己思考一下吧。

我有一個觀點,針對小數(shù)精度不夠的問題(例如 0.1),軟件可以人為的在數(shù)據(jù)最后一位補 5, 也就是 0.15,這樣犧牲一位,但是可以保證數(shù)據(jù)精度,還原再把那個尾巴 5 去掉。

請同學們思考一下。

精度在哪兒丟失?

一位熱心網(wǎng)友 獨孤小敗 在 OSC 上回復了我上一篇文章,提出了一個疑問:

在 java 中計算 0.2 + 0.4 得到的結果是

// 代碼(a)
double d = 0.2 + 0.4;  // 結果是 0.6000000000000001

但是當直接輸出 0.6 的時候,確實是 0.6

// 代碼(b)
double d = 0.6;  // 結果是 0.6

好像很矛盾。很顯然,通過代碼(b)可以知道,在 java 中,可以精確 顯示 0.6,哪怕 0.6 不能被精確表示,但至少能精確把 0.6 顯示出來,這不是和代碼(a)矛盾了嗎?

這又是一個 想當然的錯誤,在直觀上認為 0.2 + 0.4 = 0.6 是必然成立的(在數(shù)學上確實如此),既然(a)的結果是 0.6,而且 java 可以精確輸出 0.6,那么代碼(a)的結果應該輸出 0.6。

其實在計算機上 0.2 + 0.4 根本就不等于 0.6 (為什么?可以查看本系列『運算符』),因為 0.2 和 0.4 都不能被精確表示。 浮點數(shù)的精度丟失在每一個表達式,而不僅僅是表達式的求值結果。

我們用數(shù)學中的概念類比一下,比如四舍五入,我們計算 1.6 + 2.8 保留整數(shù)。

1.6 + 2.8 = 4.4 

四舍五入得到 4。我們用另一種方法

先把 1.6 四舍五入為 2
再把 2.8 四舍五入為 3
最后求和 2 + 3 = 5

通過兩種運算,我們得到了兩個結果 4 和 5。同理,在我們的浮點數(shù)運算中,參與運算的兩個數(shù) 0.2 和 0.4 精度已經(jīng)丟失了,所以他們求和的結果已經(jīng)不是 0.6 了。

后記

上面一直在討論小數(shù),整數(shù)呢?在博客園,一位童鞋為下面的代碼抓狂了:

JSON.parse('{"status":1,"id":9986705337161735,"name":"test"}').id; 

把這段代碼復制到 Chrome 的 Console 中,按回車, 詭異的問題出現(xiàn)了 9986705337161735 居然變成了 9986705337161736!原始數(shù)據(jù)加了 1。

9986705337161735
9986705337161736

一開始以為是溢出,換了個更大的數(shù):9986705337161738 發(fā)現(xiàn)不會出現(xiàn)這個問題。

但是 9986705337161739 輸出又變成了 9986705337161740!

9986705337161739
9986705337161740

測試幾次之后發(fā)現(xiàn)瀏覽器輸出數(shù)字的一個規(guī)律(justjavac注:其實這個規(guī)律是錯誤的):

  1. 十位數(shù)為偶數(shù),個位數(shù)為奇數(shù)時會減 1,個位數(shù)為奇數(shù)時會加1
  2. 十位數(shù)為奇數(shù),個位數(shù)為奇數(shù)時會加 1,個位數(shù)為奇數(shù)時會減1

又多測了幾次,發(fā)現(xiàn)根本沒有規(guī)律,很混亂?。∮袝r候是加,有時候是減!!

解析

這顯然不僅僅是丟失精度的問題,欲知后事如何…咳咳…靜待下一篇吧。

某網(wǎng)友回復

hjiayz

我覺得作者寫的很好,不過有些問題說的不夠清晰。
這些誤差都是在數(shù)制轉換中產(chǎn)生的,而電腦在實際的浮點運算中并不存在數(shù)制轉換,所以只要以16進制方式輸出和輸入就不存在誤差。
如果要以小數(shù)方式表示一個分數(shù),任何數(shù)制都可能產(chǎn)生無限循環(huán)小數(shù)。
二進制和十進制的區(qū)別在于底數(shù)為2而不是10,所以不能精確表示分母中含有5因子的分數(shù),而十進制依然無法表示分母存在7,13,等等其他因子的最簡分數(shù)。
所以如果真的要避免無限循環(huán)小數(shù),唯一的辦法是使用分數(shù)表示法。

而且實際上數(shù)制轉換中產(chǎn)生的這些誤差都非常微小。
如您的例子 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1=0.8999999999999999
實際上誤差只有0.0000000000000001
絕大多數(shù)場合這樣的誤差微不足道。
9986705337161735這個數(shù)已經(jīng)超過了32位整型數(shù)的上限,如果不出意料,最終是以浮點數(shù)的形式存在的,而把誤差和第一個例子的0.0000000000000001進行對比,很容易明白這一切的原因:那就是這根本不是一個整數(shù),而是一個浮點數(shù),并且在數(shù)制轉換中出現(xiàn)了誤差。

javascript作為一個弱類型的腳本語言雖然很不錯,但是對于沒有學習過經(jīng)典的c,pascal等強制類型語言的新手來說,類型自動轉換絕對是一個很棘手的問題。縱然js少有顯式的類型轉換,但是如果對類型轉換如果沒有深刻的了解顯然會導致很多問題,作者文章中的許多問題就來源于此。(超出整型范圍后的隱式轉換,包括任何含有小數(shù)點的數(shù),以及超過int32范圍的數(shù))

事實上對于9986705337161735這種超長的id,如果加上引號,用字符串表示,或者進行一定的處理,轉為數(shù)組儲存,都是毫無問題的。
計算機中對于精度要求很高的計算,必然需要運用到高精度庫,浮點運算的作用原本就不在于精度。事實上浮點數(shù)被廣泛使用的原因在于日常使用中,這種程度的精度已經(jīng)足夠了,比如圓周率通常運算中也只以3.1415926計算。

    本站是提供個人知識管理的網(wǎng)絡存儲空間,所有內容均由用戶發(fā)布,不代表本站觀點。請注意甄別內容中的聯(lián)系方式、誘導購買等信息,謹防詐騙。如發(fā)現(xiàn)有害或侵權內容,請點擊一鍵舉報。
    轉藏 分享 獻花(0

    0條評論

    發(fā)表

    請遵守用戶 評論公約

    類似文章 更多

    91久久精品国产成人| 国产一级内射麻豆91| 欧美日本亚欧在线观看| 久久热在线免费视频精品| 亚洲av一区二区三区精品| 久久一区内射污污内射亚洲| 久久99这里只精品热在线| 国产免费自拍黄片免费看| 樱井知香黑人一区二区| 亚洲二区欧美一区二区| 精品日韩国产高清毛片| 国产精品午夜性色视频| 日韩人妻中文字幕精品| 久久女同精品一区二区| 中文字幕在线区中文色| 五月婷婷六月丁香在线观看| 成人免费高清在线一区二区| 五月婷日韩中文字幕四虎| 国产亚洲不卡一区二区| 风间中文字幕亚洲一区| 欧美黑人黄色一区二区| 亚洲国产日韩欧美三级| 五月天丁香婷婷一区二区| 国产午夜精品亚洲精品国产| 精品国自产拍天天青青草原| 国产女同精品一区二区| 精品熟女少妇av免费久久野外| a久久天堂国产毛片精品| 国产香蕉国产精品偷在线观看| 国产乱淫av一区二区三区| 深夜视频在线观看免费你懂| 国产精品午夜性色视频| 国产又大又硬又粗又湿| 国产成人精品视频一区二区三区| 欧美韩日在线观看一区| 日韩一级毛一欧美一级乱| 91欧美亚洲精品在线观看| 一区二区三区亚洲国产| 色婷婷国产精品视频一区二区保健| 欧美国产亚洲一区二区三区| 欧美日韩久久精品一区二区 |