組成原理(三)下——浮點數運算
浮點數的表示
基礎原理
有很多數字非常的大或者非常的小,會超出32位2進制數的表示範圍。所以僅使用整形變量或是定點數並不能滿足我們的需求,所以我們有了一套由十進制的科學計數法推廣而來的浮點數表示方法。首先,我們來回顧一下十進制下的科學計數法:
我們把它推廣到R進制,就可以得到:
我們給他們都來命一個名:
- R:基數,對於二進計數值的機器是一個常數,一般規定爲2、8、16中的一個
- e:指數
- M:尾數
一個機器浮點數由階碼和尾數以及符號位組成:
- 尾數:用定點小數表示,給出有效數字的位數
- 階碼:用定點整數形式表示,知名小數點在數據中的位置,決定了浮點數的表示範圍。
通俗的說,就是:階碼控制一個範圍我們可以將這個範圍看作一個區間,然後尾數,也就是這個定點小數表示當前的數字在這個區間中的相對位置。
IEEE754(重點)
標準表示方法:爲便於軟件移植,使用 IEEE標準IEEE754 :(重點)
- 尾數用原碼;
- 階碼用變形移碼;
- 基爲2
按照該標準,32位浮點數和64位浮點數的標準格式爲:
其中:
- S—尾數符號,0正1負
- M—尾數,純小數表示,小數點放在尾數域的最前面,採用源碼錶示
- E—階碼,採用移碼錶示(後面填坑)
浮點數的規格化表示
衆所周知,一個數可以由很多種科學計數法來表示,比如:
我們可以一直寫下去,那顯然,如果大家都亂寫就亂套了,所以爲了提高數據的表示精度,需要做規格化處理,這樣做可以:
- 提高表示精度
- 使數據表示有唯一性
Q:什麼是規格化?
A:如果尾數爲R進制的規格化,絕對值大於或等於
二進制源碼的規格化數的表現形式:
正數:
負數:
補碼位數的規格化的表現形式:
正數:
負數:
補碼下尾數的最高位域符號位相反
可能有人看到這裏看不懂,我解釋一下,規格化處理後的數字應該滿足尾數的絕對值大於或等於這裏有,所以我們規格化處理後,尾數的絕對值應當大於等於0.5,0.5是什麼?是2的-1次方,那麼二進制源碼錶示下當尾數的最高位爲1時,就滿足規格化,這樣的設計便於判斷。
爲什麼這樣的設計能夠提高精度?
要想知道爲什麼這樣做能夠提高精度,就應當先了解爲什麼會產生精度誤差。產生精度誤差的原因是:**當我們要表示的數很小或不能被準確表示的時候,我們的區間長*比例只能達到近似解。**那麼我們分析以下兩種情況:
- 例子:表示0.0001
- 大區間小比例:
- 假設我們的指數爲表示出來的值爲100,那麼我們想要表示0.0001,就需要讓尾數位爲:0.000001,但是衆所周知,二進制數在很多情況下都只能儘量地逼近這個數,那麼問題來了:該怎麼讓尾數儘可能地逼近這個數呢?答案就是:儘量用更多的位。那麼反過來觀察我們的方法,我們浪費了尾數位的高位。這顯然是不理智的。
- 小區間大比例:
- 我們剛纔悟出了一個道理:精度主要由尾數位的準確程度決定,那麼我們就要儘量的壓榨尾數位,什麼時候才能把它壓榨到極限呢?答案很簡單:第一位不爲0。
隱藏位計數
我們還可以再使用隱藏位技術再提高一點精度,既然我們都知道,尾數的第一位必爲1了,那我們還記錄他幹什麼呢?那麼我們在記錄尾數的時候,可以將尾數的最高位通過左移一位隱藏起來(其實就是自然溢出),這樣就可以再多記一位尾數了。
規格化浮點數的真值
我們通過32位浮點數來看:
移碼定義:
KaTeX parse error: No such environment: align at position 8:
\begin{̲a̲l̲i̲g̲n̲}̲
\begin{array}
…
實際上就是:在的基礎上進行平移。
一個規格化的32位浮點數的x的真值爲:
一個規格化的64位浮點數x的真值爲:
例題1
若浮點數x的二進制儲存格式爲:求其32位浮點數的十進制格式:
例題2
將十進制數20.59375轉換成32位浮點數的二進制格式來存儲。
首先先將整數部分和分數部分轉化成二進制數:
移動小數點,使其在1,2位之間:
得到:
最後得到32位浮點數的二進制儲存格式爲:
IEEE754浮點數的範圍
浮點數的加減
加法
我們先從算數式的角度去理解,假設我們已經有了兩個浮點數,它們分別是:
其中,E代表階碼,M代表尾數
那麼兩個浮點數進行加減運算實際上就是:
說白了就是分爲幾步:
- 一、將兩個數對齊,對齊到階碼大的數那裏
- 二、將對齊後的尾數相加
- 三、乘上較大的階碼錶示的數
那麼我們將這個操作轉化到機器上,就變成了:
-
一、0操作數的檢查:
-
二、比較階碼大小並完成對階(小的數向大的數對齊),小階的尾數右移,每右移一位,其階碼加1(右規)。
-
三、尾數進行加或減運算
-
四、結果規格化:尾數運算完之後可能不滿足規格化的要求,所以要進行規格化處理
- 向左規格化:如果不是規格化的數,需要尾數向左移位,這個很好理解,就是太小了需要大一點,實現方法就是:每向左一位,階碼減1,直到滿足規格化要求爲止
- 向右規格化:兩個滿足規格化的數相加,可能太大。所以需要用右移的方法使結果滿足規格化要求。方法是:每右移一位,階碼加一,直到滿足規格化要求爲止。
-
五、舍入處理:在對階或向右規格化的時候,尾數要向右進位,這樣被右移的尾數的低位部分會被丟掉,從而會造成一定的誤差,所以要進行舍入處理。
- 0舍入1法:
- 如果右移的時候被丟掉的位數的最高位爲0就捨去,否則就加一
- 恆置1法:
- 只要數位被移掉,就在尾數的末尾置1。
- 0舍入1法:
-
六、溢出處理:將數字右規後,再根據階碼來判斷浮點運算是否溢出,溢出可以分爲三類:
- 負上溢:
- 正上溢:正負上溢都一樣,就是+1操作的時候,沒法再加了,就溢出了
- 下溢:向左規格化的時候,階碼已經小於0了,但是仍然要減1,這時叫做下溢
所以我們說,浮點數的溢出是以其階碼溢出表現出來的:
- 階碼下溢是由於表示的數的絕對值太小了,這時可以把數看作0
- 階碼上溢是由於表示的數的絕對值實在是太大了,可以把數看作無窮
流程圖就直接copy老師的了:
例題
硬件電路
浮點數的乘除運算
乘法運算規則:
除法規則:
移碼的溢出判斷
首先明確:使用雙符號位的階碼加法器,規定移碼的第二位始終爲0,那麼移碼的第二位爲1的話就說明他溢出了。
- 當低位符號爲0時,(10)表明結果上溢
- 當低位符號位1時,(11)表明結果下溢
- 最高位爲0,說明沒溢出
- 低位符號爲1,(01)結果爲正
- 低位結果爲0,(00)結果爲負
尾數處理
浮點加減法對結果的規格化及舍入處理也適用於浮點乘除法。
例題
浮點數表示和算法總結
- IEEE754
- 尾數規格化
- 浮點數計算流程
- 浮點數計算硬件實現
MIPS中的浮點數指令
在mips中有32個單精度的寄存器,專門用於浮點數的操作。記作:,在儲存單精度浮點數的時候,我32個可以分別使用,而在儲存雙精度浮點數的時候可以兩兩組合使用,也就是。
單精度的加減乘除指令:
add.s, sub.s, mul.s, div.s
# 示例:add.s $f0, $f1, $f6
雙精度的加減乘除:
add.d, sub.d, mul.d, div.d
# 實例:mul.d $f4, $f4, $f6
值得注意的是,上面說過雙精度浮點數儲存在兩個連續的寄存器中,我們一般用開頭的那個寄存器來代指另這兩個寄存器的總體,所以我們上面的加法中的f4實際上是指的f4f5連接而成的雙精度數,當然f6也是同理。
例題1
float f2c (float fahr){
return ((5.0/9.0) * (fahr - 32.0));
}
MIPS:
f2c : lwc1 $f16, const5($gp)# 從環境中把5讀入
lwc2 $f18, const9($gp)# 從環境中把9讀入
div.s $f16, $f16, $f18# 除法
lwc1 $f18, const32($gp)# 將32讀入
sub.s $f18, $f12, $f18# 減法
mul.s $f0, $f16, $f18# 乘法
jr $ra# 返回
例題2
void mm(double x[][], double y[][], double z[][]){
for(int i = 0; i != 32; i++){
for (int j = 0; j != 32;j ++){
for(int k = 0; k != 32; k++){
x[i][j] = x[i][j] + y[i][k] * z[k][j];
}
}
}
}
假設x,y,z分別在$a0, $a1, $a2,並且i,j,k在$s0,$s1,$s2。
MIPS:
li $t1, 32 # t1 = 32,用於判斷是否臨界
li $s0, 0 # i = 0,對i進行初始化
L1:
li $s1, 0 # j = 0,
L2:
li $s2, 0 # k = 0
sll $t2, $s0, 5 # t2 = i*32,左移五位也就是*32
addu $t2, $t2, $s1 # t2 = i*size(row) + j,到此爲止,t2 就是 (i,j)的位置了
sll $t2, $t2, 3 ## t2 = t2 * 8, 因爲每個字節八位,所以要乘八
l.d $f4, 0($t2) #f4 = x[i][j]
L3:
sll $t0, $s2, 5 # t0 = k*32,左移五位也就是*32
addu $t0, $t2, $s1 # t0 = k*size(row) + j,到此爲止,t2 就是 (i,j)的位置了
sll $t0, $t0, 3 ## t0 = t0 * 8, 因爲每個字節八位,所以要乘八
addu $t0, $a2, $t0 # t0 是z[k][j]的地址
l.d $f16, 0($t0) # f16 = z[k][j]
sll $t0, $s0, 5 # t0 = k*32,左移五位也就是*32
addu $t0, $t0, $s2 # t0 = k*size(row) + j,到此爲止,t2 就是 (i,k)的位置了
sll $t0, $t0, 3 ## t0 = t0 * 8, 因爲每個字節八位,所以要乘八
addu $t0, $a1, $t0 # t0 是y[i][k]的地址
l.d $f18, 0($t0)
mul.d $f16, $f18, $f16 # y[i][k] * z[k][j]
add.d $f4, $f4, $f16 # f4 = x[i][j] + y[i][k]*z[k][j]
addiu $s2, $s2, 1 # k++
bne $s2, $t1, L3 # if(k!=32) goto: L3
s.d $f4, 0($t2) # x[i][j] = $f4
addiu $s1, $s1, 1 # $j = j + 1
bne $s1, $t1, L2 # if( j!= 32) goto : L2
addiu $s0, $s0, 1# i++
bne $s0, $t1, L1# if(i!= 32) goto L1
精度誤差
GRS
雙精度的尾數只有53位,即使包含了一個恆爲1的隱藏位。但是他只能表述個數字,所以她表示的過程中一定是有一定的精度誤差的,爲了保證精度,IEEE754定義了一些額外的規則,比如這裏的GRS。其中guard、round、sticky是53位以外有額外增加的三位。
- guard,保護位
- round, 舍入位
- sticky,黏貼位
但是不是所有的處理器都實現了這樣的操作,這是因爲我們需要在硬件實現的成本、效率、市場等角度去做均衡。實際操作過程中,根據IEEE754,中間結果在右邊至少應該增加兩個附加位(guard & round):
- Guard bit(保護位):在尾數部分右邊的位
- Rounding bit(舍入位):在保護位右邊的位
附加位的作用:用於保護對階時向右移的位或運算的中間結果
附加位的處理:
- 左規的時候被移動到尾數中
- 作爲舍入的依據
舍入方式
- 一、就近舍入:舍入爲最近可表示的數
- 二、朝正無窮舍入:舍入爲Z2(正向)
- 三、朝負無窮舍入:舍入爲Z1(負向舍入)
- 四、朝0方向舍入:截去,正數:Z1;負數:取Z2;
由計算順序引起的誤差
比如說向上面的一樣,我們的單精度數能夠表示的數字個數有限,當範圍過大時,克表示的最小單位會超過1,那麼這時將1加入,1無法準確計算,只能作爲舍入依據