Brief
本來只打算理解JS中0.1 + 0.2 == 0.30000000000000004的原因,但發現自己對計算機的數字表示和運算十分陌生,于是只好惡補一下。
本篇我們一起來探討一下基礎——浮點數的表示方式和加減乘除運算。
在深入前有兩點我們要明確的:
1. 在同等位數的情況下,浮點數可表示的數值范圍比整數的大;
2. 浮點數無法精確表示其數值范圍內的所有數值,只能精確表示可用科學計數法m*2e表示的數值而已;
(如0.5的科學計數法是2-1,則可被精確存儲;而0.2則無法被精確存儲)
3. 浮點數不僅可表示有限的實數,還可以表示有限的整數。
Encode
20世紀80年代前每個計算機制造商都自定義自己的表示浮點數的規則,及浮點數執行運算的細節。而且不太關注運算的精確性,而是更多地關注速度和簡便性。
1985年左右推出IEEE 754標準的浮點數表示和運算規則,才讓浮點數的表示和運算均有可移植性。(IEEE,讀作Eye-Triple-Eee,電器與電子工程師協會)
上述的IEEE 754稱為IEEE 754-1985 Floating point,直到2008年對其進行改進得到我們現在使用的IEEE 754-2008 Floating point標準。
IEEE 754的二進制編碼由3部分組成,分別是sign-bit(符號位),biased-exponent(基于偏移的階碼域)和significant(尾數/有效數域)。
Sign-bit:0表示正,1表示負。占1bit;
Biased-exponent:首先exponent表示該域用于表示指數,也就是數值可表示數值范圍,而biased則表示它采用偏移的編碼方式。那么什么是采用偏移的編碼方式呢?也就是位模式中沒有設立sign-bit,而是通過設置一個中間值作為0,小于該中間值則為負數,大于改中間值則為正數。IEEE 754中規定bias = 2^e-1 - 1,e為Biased-exponent所占位數;
Significant:表示有效數,也就是數值可表示的精度。(注意:Significant采用原碼編碼;假設有效數位模式為0101,那么其值為0*2-1+1*2-2+0*2-3+1*2-4,即有效數域的指數為負數)
另外IEEE 754還提供4個精度級別的浮點數定義(單精度、雙精度、延生單精度和延生雙精度),單精度和雙精度具體定義如下:
Level | Width(bit) | Range at full precision | Width of biased-exponent(bit) | Width of significant(bit) |
Single Precision | 32 | 1.18*10-38 ~ 3.4*1038 | 8 | 23 |
Double Precision | 64 | 2.23*10-308 ~ 1.80*10308 | 11 | 52 |
為了簡便,下面以Single Precision來作敘述。
現在我們了解到32bit的浮點數由3部分組成,那么它們具體又有怎樣的組合規則呢?具體分為4種:
Normalized(規格化)
編碼規則
1. biased-exponent != 0 && biased-exponent != 2e-1;
2. exponent = biased-exponent - Bias,其中exponent是指實際的指數值;
3. significant前默認含數值為1的隱藏位(implied leading 1),即若significant域存儲的是0001,而實際值是10001。
Denormalized(非規格化)
用于表示非常接近0的數值和+0和-0。
+0的位模式為:0-00000000-00000000000000000000000
-0的位模式為: 1-00000000-00000000000000000000000
編碼規則
1. biased-exponent == 0 ;
2. exponent = 1 - Bias,其中exponent是指實際的指數值;
3. significant前默認含數值為0的隱藏位(implied leading 1),即若significant域存儲的是0001,而實際值是00001。
Infinity(無限大)
用于表示溢出。
編碼規則
1. biased-exponent == 2e-1 ;
2. significant == 0。
運算結果為Infinity的表達式
1. Infinity == N/0,其中N為任意正數;
2. -Infinity == N/-0,其中N為任意正數;
3. Infinity == Infinity * N,其中N為任意正數。
NaN(非數值)
用于表示 結果既不是 實數 又不是 無窮。
編碼規則
1. biased-exponent == 2e-1 ;
2. significant != 0。
運算結果為NaN的表達式
1. Math.sqrt(-1)
2. Infinity - Infinity
3. Infinity * 0
4. 0/0
注意
1. NaN與任何數值作運算,結果均為NaN;
2. NaN != NaN;
3. 1 == Math.pow(NaN, 0)。
Rounding modes(aka Rounding scheme,舍入模式)
由于浮點數無法精確表示所有數值,因此在存儲前必須對數值作舍入操作。具體分為以下5種舍入模式
1. Round to nearest, ties to even(IEEE 754默認的舍入模式)
舍入到最接近且可以表示的值,當存在兩個數一樣接近時,取偶數值。(如2.4舍入為2,2.6舍入為3;2.5舍入為2,1.5舍入為2。)
Q:為什么會當存在兩個數一樣接近時,取偶數值呢?
A:由于其他舍入方式均令結果單方向偏移,導致在運算時出現較大的統計偏差。而采用這種偏移則50%的機會偏移兩端方向,從而減少偏差。
2. Round to nearest, ties to zero
舍入到最接近且可以表示的值,當存在兩個數一樣接近時,取離0較遠的數值
3. Round to infinity
向正無窮方向舍入
4. Round to negative infinity
向負無窮方向舍入
5. Round to zero
向0方向舍入
Overflow
到這里我們對浮點數的表示方式已經有一定程度的了解了,也許你會迫不及待想了解它的運算規則,但在這之前我想大家應該要想理解溢出和如何判斷溢出,不然無法理解后續對運算的講解。
Q1:何為溢出?
A1:溢出,就是運算結果超出可表示的數值范圍。注意:進位、借位不一定會產生溢出。
發生溢出: 4位有符號整數 7+7 0111 + 0111 1110 => -2 只有最高數值位發生進位,因此發生溢出 沒有發生溢出: 4位有符號整數 7-2 0111 + 1110 10101 => 取模后得到5 符號位和最高數值位均發生進位,因此沒有發生溢出
Q2:如何判斷溢出?
A2:有兩種方式判斷溢出,分別是 進位判斷法 和 雙符號判斷法。
進位判斷法
前提:采用單符號位,即+4的二進制位模式為0100。
無溢出:符號位 和 數值域最高位 一起進位或一起不發生進位。
溢出:符號位 或 數值域最高位 其中一個發生進位。
雙符號判斷法
前提:采用雙符號位,即+4的二進制位模式為00100。
無溢出:兩符號位相同。
上溢出:兩符號位為01。
下溢出:兩符號位為10。
Q3:溢出在有符號整數和浮點數間的區別?
A3:對于有符號整數而言,溢出意味著運算結果將與期待值不同從而導致錯誤;
對于浮點數而言,會對上溢出和下溢出進行特殊處理,從而返回一個可被IEEE 754表示的浮點數。
因此對于有符號整數的運算我們采用進位判斷法判斷溢出即可,而對于浮點數則需要采用雙符號判斷法了。
Q4:浮點數運算中上溢出和下溢出具體的特殊處理是什么啊?
A4:首先浮點數運算中僅對階碼進行溢出判斷,當階碼出現下溢出時運算結果為0(符號取決于符號位);當階碼出現上溢出時運算結果為Infinity(符號取決于符號位)。
PS:發生溢出時,當前程序會被掛起,并發送溢出中斷信號量,此時溢出中斷處理程序接收信號量后會做對應的處理。
Addition & Subtraction
恭喜你還沒被上述的前置知識搞暈而選擇X掉網頁,下面我們終于可以著手處理運算問題,順便驗證一下自己對上述內容是否真的理解了。
步驟:
1. 對0、Infinity和NaN操作數作檢查
若有一個操作數為NaN則直接返回NaN;
若有一個操作數為0則直接返回另一個操作數;
若有一個操作數為Infinity,
若另一個操作數也是Infinity且符號相同則返回第一個操作數;
若另一個操作數也是Infinity且符號不同則返回NaN;
若其他情況則返回Infinity。
2. 對階,若兩操作數的階碼不相等,則對階(小數點對齊)。
由于尾數右移所損失的精度比左移的小,因此小階向大階看齊。
3. 符號位+尾數(包含隱藏位)執行加減法。
按有符號整數加減法進行運算,并采用雙符號法判斷是否溢出。
4. 規格化。
向右規格化:若步驟3出現溢出,則尾數右移1位,階碼+1;
向左規格化:若步驟3沒有出現溢出,且數值域最高位與符號位數值相同,則尾數左移1位且階碼-1,直到數值域最高位為1為止。
5. 舍入處理
6. 溢出判斷
由于規格化時可能會導致階碼發生溢出
若無發生溢出,則運算正常結束。
若發生上溢出,則返回Infinity。
若發生下溢出,則返回0。
示例1,0.75+(-0.75) = 0:

0.75+(-0.75) = 0 以8位存儲,尾數采用原碼編碼 0.75 存儲位模式 0-0110-100 -0.75 存儲位模式 1-0110-100 1. 對階 階碼一樣跳過。 2. 符號位+尾數(含隱藏位)相加 由于尾數以有符號數的方式進行運算,因此要對尾數進行取補操作 00-1100 +11-0100 100-0000 對符號位截斷后得到00-0000 3. 規格化 根據符號位相同,則執行左規格化操作,也就是尾數不斷向左移而階碼不斷減1,直到尾數最高位為1為止。 4. 舍入處理 000 5.溢出判斷 在執行規格化時發生階碼下溢出,整體結果返回0-0000-000.
示例2,0.75-0.25 = 0.5:

0.75-0.25 = 0.5 以8位存儲,尾數采用原碼編碼 0.75 存儲位模式 0-0110-100 0.25 存儲位模式 0-0101-000 1. 對階 0.25的階碼小于0.75相同,因此0.25向0.75的看齊。 0-0110-100 2. 尾數相加(采用雙位符號法),減法轉換為加法,對0.75和-0.25取補 00-1100 +11-1100 100-1000 對符號位截斷后得到00-1000 3. 規格化 根據符號位相同,則執行左規格化操作,也就是尾數不斷向左移而階碼不斷減1,直到尾數最高位為1為止。 4. 舍入處理 1000 5. 溢出判斷 階碼沒有發生溢出,正常返回運算結果0-0110-000(注意:舍入處理后數值域的最高位是位于隱藏位的)
示例3, 0.25-0.75 = -0.5:
Comparison
比較運算(cmp指令)實際上就是對兩操作數做減法,然后通過標志寄存器(80x86的rflags)中的CF(Carry flag)、ZF(Zero flag)、OF(Overflow flag)、SF(Sign flag)狀態標志來判斷兩者的關系。
對于無符號數A與B而言,則是通過CF和ZF來判斷。
1. 若CF為1,表示A-B時A發生借位操作,那么可以判定 A<B
2. 若CF為0且ZF不為0,表示A-B時沒有發生借位操作,那么可以判定 A>B
3. 若ZF為0,則可判定A==B
對于有符號數C和D而言,則是通過ZF、OF和SF來判斷。
1. 若ZF為1,則可判定C == D
2. 若SF為0,OF為0,表示結果為正數且沒有發生溢出,則C>D
3. 若SF為0,OF為1,表示結果為正數且發生溢出,則C<D
4. 若SF為1,OF為0,表示結果為負數且沒有發生溢出,則C<D
5. 若SF為1,OF為1,表示結果為負數且發生溢出,則C>D
而對于浮點數而言,由于階碼域采用的是biased-exponent的編碼方式,因此在進行比較時我們可以將整個浮點數看作有符號數來執行減法運算即可,而不必執行Addition/Subtraction那樣繁瑣的運算步驟。
Multiplication
步驟:
1. 對0、Infinity和NaN操作數作檢查
若有一個操作數為NaN則直接返回NaN;
若有一個操作數為0則直接返回另一個操作數;
若有一個操作數為Infinity,
若另一個操作數也是Infinity且符號相同則返回第一個操作數;
若另一個操作數也是Infinity且符號不同則返回NaN;
若其他情況則返回Infinity。
2. 計算階碼(公式:e = e1 + e2 - Bias)
公式推導過程:

∵ E1 = e1 - Bias ∵ E2 = e2 - Bias ∴ E = E1+E2 = e1 + e2 - 2*Bias ∵ e = E + Bias ∴ e = e1 + e2 - Bias 對非規格化e1為1
注意:計算階碼時,是執行無符號數的加減法。
3. 尾數相乘
注意:尾數相乘時,是執行無符號數的乘法,并且不對結果進行截斷。
4. 結果左規格化,尾數左移,且階碼減1,直到最高位為1為止
5. 舍入處理
6. 溢出判斷
由于規格化時可能會導致階碼發生溢出
若無發生溢出,則運算正常結束。
若發生上溢出,則返回Infinity。
若發生下溢出,則返回0。
7. 符號位執行 異或 運算
示例,0.5*(-0.25) = -0.125:

0.5*(-0.25) = -0.125 以8位存儲,尾數采用原碼編碼 0.5 存儲位模式 0-0110-000 -0.25 存儲位模式 1-0101-000 1. 計算階碼(公式e1+e2-Bias) 0110 +0101 1011 +1001 10100 取模得到 0100 2. 尾數相乘 1000*1000 1000<<(3+1) - 1000<<(3) 1000000 - 0100000 減法轉換為加法 1000000 +1100000 10100000 取模得到0100000 3. 左規格化 0100000左移1位得到 100000 階碼-1得到 0011 4. 舍入處理 尾數為1000 5. 溢出判斷 無溢出 6. 符號位異 0 xor 1 = 1 結果 1-0011-000
Divsion
步驟:
1. 對0、Infinity和NaN操作數作檢查
若有一個操作數為NaN則直接返回NaN;
若有一個操作數為0則直接返回另一個操作數;
若有一個操作數為Infinity,
若另一個操作數也是Infinity且符號相同則返回第一個操作數;
若另一個操作數也是Infinity且符號不同則返回NaN;
若其他情況則返回Infinity。
2. 計算階碼(公式:e = e1 - e2 + Bias + m-1) m為尾數的位數
公式推導過程:

∵E1 = e1 - Bias ∴E2 = e2 - Bias ∴E = E1-E2 = e1 - e2 ∵e = E + Bias ∴e = e1 - e2 + Bias ∵除法會消權,因此通過移位去除負指數以便后續計算 ∴e = e1 - e2 + Bias + m-1,其中m為尾數的位數 對非規格化e1、e2為1
注意:計算階碼時,是執行無符號數的加減法。
3. 尾數相除
注意:尾數相除時,是執行無符號數的除法,并且不對結果進行截斷。
4. 結果左規格化,尾數左移,且階碼減1,直到最高位為1為止
5. 舍入處理
6. 溢出判斷
由于規格化時可能會導致階碼發生溢出
若無發生溢出,則運算正常結束。
若發生上溢出,則返回Infinity。
若發生下溢出,則返回0。
7. 符號位執行 異或 運算
示例,0.5/(-0.25) = -2:

0.5/(-0.25) = -2 以8位存儲,尾數采用原碼編碼 0.5 存儲位模式 0-0110-000 -0.25 存儲位模式 1-0101-000 2. 計算階碼(公式e1-e2+Bias+(m-1)) 0110 +1011 10001 +00111 11000 +00011 11011 取模得到 1011 3. 尾數相除 1000/1000 = 1000 >> 3 得到0001 4. 左規格化 0001左移3位得到 1000 階碼-3得到 1000 5. 舍入處理 尾數為1000 6. 溢出判斷 無溢出 7. 符號位異 0 xor 1 = 1 結果 1-1000-000
Conclusion
總算寫完了:)本文以單精度作為敘述對象,為簡化手工運算各示例均以8bit浮點數作為講解,其實32bit和64bit的浮點數表示和運算規則與其相同,理解規則就OK了。
看完這么多原理性的東西,是時候總結一下我們對浮點數應有的印象了:
1. 浮點數可表示的值范圍比同等位數的整數表示方式的值范圍要大得多;
2. 浮點數無法精確表示其值范圍內的所有數值,而有符號和無符號整數則是精確表示其值范圍內的每個數值;
3. 浮點數只能精確表示m*2e的數值;
4. 當biased-exponent為2e-1-1時,浮點數能精確表示該范圍內的各整數值;
5. 當biased-exponent不為2e-1-1時,浮點數不能精確表示該范圍內的各整數值。
例如:1000000000000000128
與1000000000000000129以雙精度浮點數表示時,均為
0-10000111010-11011110000010110110101100111010011101100100000000001。
若以64bit無符號整數表示時,1000000000000000128為 0000110111100000101101101011001110100111011001000000000010000000;
1000000000000000129為 0000110111100000101101101011001110100111011001000000000010000001。
例子源自:http://es5.github.io/#x15.7.4.5
尊重原創,轉載請注明來自:http://www.cnblogs.com/fsjohnhuang/p/5109766.html 肥子John^_^
Thanks
https://en.wikipedia.org/wiki/IEEE_754-1985
http://geeklu.com/2011/03/ieee754-floating-point-arithmetic/
http://blog.csdn.net/hillchan31/article/details/7565782
http://blog.csdn.net/jn1158359135/article/details/7761011
http://laokaddk.blog.51cto.com/368606/284280/
《深入理解計算機系統》
文章列表