2016-04-26 發佈初始版本php
2016-06-13 更新了非規則浮點數內容html
以前在寫某個迭代算法的時候,發現算法在某些狀況下會出錯,後來調試過程當中發現,計算過程當中,某些理論上大於0的數值會在迭代過程當中變爲0,最後計算過程當中出現了除0,致使結果出錯。這篇文章的初始目的就是爲了闡明爲什麼某些理論上大於0的數在實際計算中會變爲0(下溢),後來順便將不少人討論過數據類型轉換、運算精度也寫進去了。在我看來這是一個很差闡述的話題,我從數字信號處理中的量化出發,試圖給出一個較爲直觀的認識。文章可能還有一些問題,還請批評指正。算法
數字信號處理中的量化指將輸入信號從一個大的集合映射到一個的小集合的過程。能夠簡單的、狹義的理解爲將一個連續的量映射到離散的集合上的過程。以下圖所示,紅色曲線是輸入信號,經過3比特量化獲得的結果爲藍色曲線。svg
(By Hyacinth - Own work, CC BY-SA 3.0, https://commons.wikimedia.org/w/index.php?curid=30716342)測試
天然的,能夠引出三個問題this
爲什麼要將輸入信號量化編碼
接收到的信號,譬如通訊過程當中的電磁波,通常將其視做模擬量(時間上連續,取值連續),爲了存儲、計算這些信號,須要經過採樣、量化將其變爲數字量。量化其實是出於兩個考慮,其一是存儲、其二是計算。未經量化的信號沒法存儲在存儲器中,同時也沒法進行計算。spa
量化對信號自己有何影響設計
量化過程當中,輸入信號的集合每每是不可數的(或者集合有無窮元素),量化輸出信號的集合是有限的。這也就意味着量化是一個不可逆的過程,這天然會對信號有影響。以下圖所示,量化過程會帶來量化噪聲(偏差,即量化先後信號的差值),即量化後信號會有失真,沒有額外先驗知識的狀況下,失真是沒法恢復的。3d
By Gregory Maxwell - http://wiki.xiph.org/File:Dsat_011.png, CC BY 3.0, https://commons.wikimedia.org/w/index.php?curid=26171868
如何對輸入信號進行量化 (注:此處僅討論標量量化)
上面的兩幅圖中,量化是經過將輸入範圍劃分爲若干個區域,對於落入同一區域內的信號賦值爲同一個(二進制)數。區域的劃分是均勻的,這一量化關係能夠表示爲下圖。(輸入是連續值,對應輸出是離散值)
不一樣輸入信號取值下,量化偏差是相同的,這種方式被稱爲均勻量化。可是不少狀況下,咱們更關注相對的量化偏差(量化偏差/信號取值),即對於小信號而言,量化偏差較小,而大信號能夠具備相對較大的量化偏差。這一狀況下能夠採用不一樣的量化方式,以下圖所示(注:這是我瞎編的,有對應的非均勻量化的標準)。
非均勻量化能夠得到更高的信噪比,兩種不一樣的量化方式具備不一樣的應用。除此以外還有其餘量化方式,此處再也不說明。
儘管上節討論的是數字信號處理中的量化,可是量化、或是相似量化的操做實際上出如今不少地方。譬如計算機的存儲空間是有限的,32比特的存儲空間僅僅只可以表示種不一樣的可能。然而不少狀況下,咱們所期待的運算是在實數域上進行的,而相似數字信號處理中的狀況,計算機只能對量化後的信號進行存儲和計算。
數據的存儲,設計到數據類型,這裏討論兩種:整型(integer)和浮點型 (float)。此處,相似量化的思路,咱們認爲計算機將實數域上的輸入,量化爲整型/浮點型進行表示。
輸入實數域數值,量化範圍爲,量化方式爲均勻量化,假設爲量化結果,則有
譬如,,則取。對不一樣的,量化偏差是一個恆定的取值。
浮點和整型比稍微複雜一些,參考維基百科, 32比特浮點數的存儲方式表示以下圖。
對應浮點數取值可表示爲(十進制)
大於0的浮點數依次爲,然而大於1的浮點數依次爲,即量化間隔是不一樣的,實際上,量化精度和數據大小的關係可表示爲
即將一個實數域上的數存儲爲浮點表示,能夠看做是一個非均勻量化的過程。
注1:本節中的量化,實際上應該是量化和編碼兩個過程,不只僅將數值量化了,同時採用相應的編碼方式編碼存儲。
注2:數據類型的定義比本文描述要複雜,由於設計到0,無窮和非數的處理。
注3:不一樣運行環境,對於float的定義不盡相同。
相似下面的代碼在博客園討論過不少次了,即
float a = (float) 10.375; float b = (float) 2.263; System.out.println(a+b);
這一代碼在個人機器上運行結果輸出爲12.6380005。雖然鮮有人討論關於(int) 10.375+ (int) 2.263 = 12這個式子,可是不管是整型仍是浮點類型,出現這個問題的原因都是同樣的——咱們任意在實數域(或是有理數域)選擇了兩個數,然而計算機中存儲的是量化以後的結果,不管對integer或是float都是這樣。只是咱們習慣性的認爲float強大到無所不能,可是它的表示能力依舊是有限的。
第一節中討論了量化先後信號的變化,會帶來量化噪聲。同理,咱們給出的數,譬如10.375和2.263,利用浮點存儲一樣會帶來噪聲。而計算結果和理論值的差距就是這個噪聲的直接體現。
咱們並不可以保證,所以計算看似出錯了,實際上只是計算機按本身邏輯計算出現的偏差。
相似float→double,或是int→long此類的類型轉換,或是量化區間增大了,或是量化精度提高了,轉化過程不會引起任何問題,簡單舉例(示意圖)。相似這樣的量化關係,從int→long→int,或是float→double→float,轉化不會進一步引入噪聲。
然而以下的轉化則否則,即int→float→int
int a = 200000002; float b = (float) a; int c = (int)b; System.out.println(c);
輸出結果爲200000000;轉換過程的示意圖可表示爲
上溢(Arithmetic overflow),即運算結果超出了寄存器或存儲空間所能存儲或表示的範圍。從量化的角度來看,能夠認爲是超過了量化範圍,上溢通常很容易被發現,但有時也會被忽略。譬如,leetcode的第一題,一個有一些問題的代碼也可以經過測試
Given an array of integers, return indices of the two numbers such that they add up to a specific target.
public int[] twoSum(int[] nums, int target) { for (int i = 0; i < nums.length; i++) { for (int j = i + 1; j < nums.length; j++) { if (nums[j] == target - nums[i]) { return new int[] { i, j }; } } } throw new IllegalArgumentException("No two sum solution"); }
上面的代碼是提供的解答方式,可是因爲nums是int類型的,target也是int類型的,所以target-nums[i]可能會overflow,致使出現錯誤的結果。
相對而言,「下溢」就隱蔽不少了,下溢(Arithmetic underflow)很難發現,也很很差處理。這裏的underflow不是指數據小於所能表示的最小值,這種狀況,譬如-129再也不int8的表示範圍,應該被歸類到overflow,即「運算結果超出了寄存器或存儲空間所能存儲或表示的範圍」。
浮點數設計過程當中,數據越大,量化精度越低,然而有一個例外,即0附近。32比特浮點數,和0最近的正常數爲(不一樣標準不一樣),然而比最接近的數爲。這意味着0附近的量化精度是相對較低的,相對較低的問題並不會帶來過多的問題,可是一旦一個非0的數據因爲足夠小,被存儲爲0,則可能會帶來一系列問題。所以標準中定義了Denormal number,但這依舊沒法完全解決問題,只要一個數足夠小,就會被下溢爲0,而在迭代算法中,這種狀況頗有可能會發生。若是不幸這個數被做爲了除數,那麼就會出現除0的狀況,這可能致使錯誤 。譬如
public static void main(String[] args) { float a = Float.MIN_VALUE; float b = Float.MIN_VALUE/2; System.out.println(b>0); System.out.println(a/b); System.out.println(b/b); }
這段代碼的運行結果爲
false
Infinity
NaN
NaN,即「非數」和任意數值計算結果均爲NaN,這是在計算過程當中不指望發生的。上面這段代碼中下溢很明顯,可是在不少迭代算法中,卻很難判斷下溢的產生,此時咱們須要根據狀況採用不一樣的處理方式防止下溢致使的錯誤,這再也不本文的討論範圍內。
更多的關於normal number和Denormal number的討論可參見非規則浮點數和規則浮點數。
值得關注的問題是,談論了那麼多關於量化噪聲的問題,那麼,計算機的計算結果還靠譜嗎?即
計算機的計算結果是否能保證絕對的準確性?
在必定條件下是能夠的。回到第一節、第二節會發現討論的前提是
數字信號處理領域,接收信號是模擬的,須要經過ADC採樣量化,這時量化噪聲是必然存在的。然而,計算機中的數據可能具備不一樣的含義。譬如,採用變量a表示書本的頁數。那麼,書本的頁數必然是一個不小於0的整數,且通常而言會是有限的。那麼,此時若a採用整型存儲,就可以精確的表示書本的頁數,不引入任何偏差。
由此出發,對於不一樣的應用,若是有一些先驗知識,咱們有可能能夠設計不一樣的數據類型/結構,以及相應的計算方法,獲得準確的計算結果。