計算機組成原理筆記(二)

個人博客: https://www.luozhiyun.com/java

浮點數和定點數

咱們先來看一個問題,在Chrome瀏覽器裏面經過開發者工具,打開瀏覽器裏的Console,在裏面輸入「0.3 + 0.6」:瀏覽器

>>> 0.3 + 0.6
0.8999999999999999

下面咱們來一步步解釋,爲何會這樣。緩存

定點數

若是咱們用32個比特表示整數,用4個比特來表示0~9的整數,那麼32個比特就能夠表示8個這樣的整數。架構

而後咱們把最右邊的2個0~9的整數,當成小數部分;把左邊6個0~9的整數,當成整數部分。這樣,咱們就能夠用32個比特,來表示從0到999999.99這樣1億個實數了。工具

這種用二進制來表示十進制的編碼方式,叫做BCD編碼。這種小數點固定在某一位的方式,咱們也就把它稱爲定點數。oop

缺點:
第一,這樣的表示方式有點「浪費」。原本32個比特咱們能夠表示40億個不一樣的數,可是在BCD編碼下,只能表示1億個數。
第二,這樣的表示方式沒辦法同時表示很大的數字和很小的數字。ui

浮點數

咱們在表示一個很大的數的時候,一般能夠用科學計數法來表示。編碼

在計算機裏,我也能夠用科學計數法來表示實數。浮點數的科學計數法的表示,有一個IEEE的標準,它定義了兩個基本的格式。一個是用32比特表示單精度的浮點數,也就是咱們經常說的float或者float32類型。另一個是用64比特表示雙精度的浮點數,也就是咱們平時說的double或者float64類型。設計

單精度

單精度的32個比特能夠分紅三部分。3d

第一部分是一個符號位,用來表示是正數仍是負數。咱們通常用s來表示。在浮點數裏,咱們不像正數分符號數仍是無符號數,全部的浮點數都是有符號的。

接下來是一個8個比特組成的指數位。咱們通常用e來表示。8個比特可以表示的整數空間,就是0~255。咱們在這裏用1~254映射到-126~127這254個有正有負的數上。

最後,是一個23個比特組成的有效數位。咱們用f來表示。綜合科學計數法,咱們的浮點數就能夠表示成下面這樣:

$(-1)^s×1.f×2^e$

特殊值的表示:

以0.5爲例子。0.5的符號爲s應該是0,f應該是0,而e應該是-1,也就是

$0.5= (-1)^0×1.0×2^{-1}=0.5$,對應的浮點數表示,就是32個比特。

不考慮符號的話,浮點數可以表示的最小的數和最大的數,差很少是$1.17×10^{-38}$和$3.40×10^{38}$。

回到咱們最開頭,爲何咱們用0.3 + 0.6不能獲得0.9呢?這是由於,浮點數沒有辦法精確表示0.三、0.6和0.9。

浮點數的二進制轉化

咱們輸入一個任意的十進制浮點數,背後都會對應一個二進制表示。

好比:9.1,那麼,首先,咱們把這個數的整數部分,變成一個二進制。這裏的9,換算以後就是1001。

接着,咱們把對應的小數部分也換算成二進制。和整數的二進制表示採用「除以2,而後看餘數」的方式相比,小數部分轉換成二進制是用一個類似的反方向操做,就是乘以2,而後看看是否超過1。若是超過1,咱們就記下1,並把結果減去1,進一步循環操做。在這裏,咱們就會看到,0.1其實變成了一個無限循環的二進制小數,0.000110011。這裏的「0011」會無限循環下去。

結果就是:$1.0010$$0011$$0011… × 2^3$

這裏的符號位s = 0,對應的有效位f=001000110011…。由於f最長只有23位,那這裏「0011」無限循環,最多到23位就截止了。因而,f=00100011001100110011 001。最後的一個「0011」循環中的最後一個「1」會被截斷掉。

對應的指數爲e,表明的應該是3。由於指數位有正又有負,因此指數位在127以前表明負數,以後表明正數,那3其實對應的是加上127的偏移量130,轉化成二進制,就是130,對應的就是指數位的二進制,表示出來就是10000010。

最終獲得的二進制表示就變成了:

010000010 0010 0011001100110011 001

若是咱們再把這個浮點數表示換算成十進制, 實際準確的值是9.09999942779541015625。

浮點數的加法和精度損失

浮點數的加法是:先對齊、再計算。

那咱們在計算0.5+0.125的浮點數運算的時候,首先要把兩個的指數位對齊,也就是把指數位都統一成兩個其中較大的-1。對應的有效位1.00…也要對應右移兩位,由於f前面有一個默認的1,因此就會變成0.01。而後咱們計算二者相加的有效位1.f,就變成了有效位1.01,而指數位是-1,這樣就獲得了咱們想要的加法後的結果。

其中指數位較小的數,須要在有效位進行右移,在右移的過程當中,最右側的有效位就被丟棄掉了。這會致使對應的指數位較小的數,在加法發生以前,就丟失精度。

指令週期(Instruction Cycle)

計算機每執行一條指令的過程,能夠分解成這樣幾個步驟。

  1. Fetch(取得指令),也就是從PC寄存器裏找到對應的指令地址,根據指令地址從內存裏把具體的指令,加載到指令寄存器中,而後把PC寄存器自增,好在將來執行下一條指令。
  2. Decode(指令譯碼),也就是根據指令寄存器裏面的指令,解析成要進行什麼樣的操做,是R、I、J中的哪種指令,具體要操做哪些寄存器、數據或者內存地址。
  3. Execute(執行指令),也就是實際運行對應的R、I、J這些特定的指令,進行算術邏輯操做、數據傳輸或者直接的地址跳轉。

Fetch - Decode - Execute循環稱之爲指令週期(Instruction Cycle)。

在取指令的階段,咱們的指令是放在存儲器裏的,實際上,經過PC寄存器和指令寄存器取出指令的過程,是由控制器(Control Unit)操做的。指令的解碼過程,也是由控制器進行的。一旦到了執行指令階段,不管是進行算術操做、邏輯操做的R型指令,仍是進行數據傳輸、條件分支的I型指令,都是由算術邏輯單元(ALU)操做的,也就是由運算器處理的。不過,若是是一個簡單的無條件地址跳轉,那麼咱們能夠直接在控制器裏面完成,不須要用到運算器。

時序邏輯電路

有一些電路,只須要給定輸入,就能獲得固定的輸出。這樣的電路,咱們稱之爲組合邏輯電路(Combinational Logic Circuit)。

時序邏輯電路有如下幾個特色

  1. 自動運行,時序電路接通以後能夠不停地開啓和關閉開關,進入一個自動運行的狀態。
  2. 存儲。經過時序電路實現的觸發器,能把計算結果存儲在特定的電路里面,而不是像組合邏輯電路那樣,一旦輸入有任何改變,對應的輸出也會改變。
  3. 時序電路使得不一樣的事件按照時間順序發生。

最多見的就是D觸發器,電路的輸出信號不僅僅取決於當前的輸入信號,還要取決於輸出信號以前的狀態。

PC寄存器

PC寄存器就是程序計數器。

加法器的兩個輸入,一個始終設置成1,另一個來自於一個D型觸發器A。咱們把加法器的輸出結果,寫到這個D型觸發器A裏面。因而,D型觸發器裏面的數據就會在固定的時鐘信號爲1的時候更新一次。

這樣,咱們就有了一個每過一個時鐘週期,就能固定自增1的自動計數器了。

最簡單的CPU流程

  1. 首先,有一個自動計數器會隨着時鐘主頻不斷地自增,來做爲咱們的PC寄存器。
  2. 在這個自動計數器的後面,咱們連上一個譯碼器(用來尋址,將指令內存地址轉換成指令)。譯碼器還要同時連着咱們經過大量的D觸發器組成的內存。
  3. 自動計數器會隨着時鐘主頻不斷自增,從譯碼器當中,找到對應的計數器所表示的內存地址,而後讀取出裏面的CPU指令。
  4. 讀取出來的CPU指令會經過咱們的CPU時鐘的控制,寫入到一個由D觸發器組成的寄存器,也就是指令寄存器當中。
  5. 在指令寄存器後面,咱們能夠再跟一個譯碼器。這個譯碼器再也不是用來尋址的了,而是把咱們拿到的指令,解析成opcode和對應的操做數。
  6. 當咱們拿到對應的opcode和操做數,對應的輸出線路就要鏈接ALU,開始進行各類算術和邏輯運算。對應的計算結果,則會再寫回到D觸發器組成的寄存器或者內存當中。

指令流水線

指令流水線指的是把一個指令拆分紅一個一個小步驟,從而來減小單條指令執行的「延時」。經過同時在執行多條指令的不一樣階段,咱們提高了CPU的「吞吐率」。

若是咱們把一個指令拆分紅「取指令-指令譯碼-執行指令」這樣三個部分,那這就是一個三級的流水線。若是咱們進一步把「執行指令」拆分紅「ALU計算(指令執行)-內存訪問-數據寫回」,那麼它就會變成一個五級的流水線。

五級的流水線,就表示咱們在同一個時鐘週期裏面,同時運行五條指令的不一樣階段。

咱們能夠看這樣一個例子。咱們順序執行這樣三條指令。

  1. 一條整數的加法,須要200ps。
  2. 一條整數的乘法,須要300ps。
  3. 一條浮點數的乘法,須要600ps

若是咱們是在單指令週期的CPU上運行,最複雜的指令是一條浮點數乘法,那就須要600ps。那這三條指令,都須要600ps。三條指令的執行時間,就須要1800ps。

若是咱們採用的是6級流水線CPU,每個Pipeline的Stage都只須要100ps。那麼,在這三個指令的執行過程當中,在指令1的第一個100ps的Stage結束以後,第二條指令就開始執行了。在第二條指令的第一個100ps的Stage結束以後,第三條指令就開始執行了。這種狀況下,這三條指令順序執行所須要的總時間,就是800ps。那麼在1800ps內,使用流水線的CPU比單指令週期的CPU就能夠多執行一倍以上的指令數。

流水線設計CPU的風險

  1. 結構冒險

能夠看到,在第1條指令執行到訪存(MEM)階段的時候,流水線裏的第4條指令,在執行取指令(Fetch)的操做。訪存和取指令,都要進行內存數據的讀取。可是內存在一個時鐘週期是沒辦法都作的。

解決辦法:在高速緩存層面拆分成指令緩存和數據緩存

在CPU內部的高速緩存部分進行了區分,把高速緩存分紅了指令緩存(Instruction Cache)和數據緩存(Data Cache)兩部分。

  1. 數據冒險
    先寫後讀
int main() {
  int a = 1;
  int b = 2;
  a = a + 2;
  b = a + 3;
}

這裏須要保證a和b的值先賦,而後才能進行準確的運算。這個先寫後讀的依賴關係,咱們通常被稱之爲數據依賴,也就是Data Dependency。

先讀後寫

int main() {
  int a = 1;
  int b = 2;
  a = b + a;
  b = a + b;
}

這裏咱們先要讀出a = b+a,而後才能正確的寫入b的值。這個先讀後寫的依賴,通常被叫做反依賴,也就是Anti-Dependency。

寫後再寫

int main() {
  int a = 1;
  a = 2;
}

很明顯,兩個寫入操做不能亂,要否則最終結果就是錯誤的。這個寫後再寫的依賴,通常被叫做輸出依賴,也就是Output Dependency。

解決辦法:流水線停頓(Pipeline Stall)

若是咱們發現了後面執行的指令,會對前面執行的指令有數據層面的依賴關係,那最簡單的辦法就是「再等等」。咱們在進行指令譯碼的時候,會拿到對應指令所須要訪問的寄存器和內存地址。

在實踐過程當中,在執行後面的操做步驟前面,插入一個NOP操做,也就是執行一個其實什麼都不幹的操做。

  1. 控制冒險

在執行的代碼中,一旦遇到 if…else 這樣的條件分支,或者 for/while 循環的時候會發生相似cmp比較指令、jmp和jle這樣的條件跳轉指令。

在jmp指令發生的時候,CPU可能會跳轉去執行其餘指令。jmp後的那一條指令是否應該順序加載執行,在流水線裏面進行取指令的時候,咱們無法知道。要等jmp指令執行完成,去更新了PC寄存器以後,咱們才能知道,是否執行下一條指令,仍是跳轉到另一個內存地址,去取別的指令。

解決辦法:
縮短分支延遲
條件跳轉指令其實進行了兩種電路操做。
第一種,是進行條件比較。
第二種,是進行實際的跳轉,也就是把要跳轉的地址信息寫入到PC寄存器。不管是opcode,仍是對應的條件碼寄存器,仍是咱們跳轉的地址,都是在指令譯碼(ID)的階段就能得到的。而對應的條件碼比較的電路,只要是簡單的邏輯門電路就能夠了,並不須要一個完整而複雜的ALU。

因此,咱們能夠將條件判斷、地址跳轉,都提早到指令譯碼階段進行,而不須要放在指令執行階段。對應的,咱們也要在CPU裏面設計對應的旁路,在指令譯碼階段,就提供對應的判斷比較的電路。

分支預測
最簡單的分支預測技術,叫做「僞裝分支不發生」。顧名思義,天然就是仍然按照順序,把指令往下執行。

若是分支預測失敗了呢?那咱們就把後面已經取出指令已經執行的部分,給丟棄掉。這個丟棄的操做,在流水線裏面,叫做Zap或者Flush。CPU不只要執行後面的指令,對於這些已經在流水線裏面執行到一半的指令,咱們還須要作對應的清除操做。

動態分支預測
就是記錄當前分支的比較狀況,直接用當前分支的比較狀況,來預測下一次分支時候的比較狀況。

例子:

public class BranchPrediction {
    public static void main(String args[]) {        
        long start = System.currentTimeMillis();
        for (int i = 0; i < 100; i++) {
            for (int j = 0; j <1000; j ++) {
                for (int k = 0; k < 10000; k++) {
                }
            }
        }
        long end = System.currentTimeMillis();
        System.out.println("Time spent is " + (end - start));
                
        start = System.currentTimeMillis();
        for (int i = 0; i < 10000; i++) {
            for (int j = 0; j <1000; j ++) {
                for (int k = 0; k < 100; k++) {
                }
            }
        }
        end = System.currentTimeMillis();
        System.out.println("Time spent is " + (end - start) + "ms");
    }
}

輸出:

Time spent in first loop is 5ms
Time spent in second loop is 15ms

分支預測策略最簡單的一個方式,天然是「假定分支不發生」。對應到上面的循環代碼,就是循環始終會進行下去。在這樣的狀況下,上面的第一段循環,也就是內層 k 循環10000次的代碼。每隔10000次,纔會發生一次預測上的錯誤。而這樣的錯誤,在第二層 j 的循環發生的次數,是1000次。

最外層的 i 的循環是100次。每一個外層循環一次裏面,都會發生1000次最內層 k 的循環的預測錯誤,因此一共會發生 100 × 1000 = 10萬次預測錯誤。

操做數前推

經過流水線停頓能夠解決資源競爭產生的問題,可是,插入過多的NOP操做,意味着咱們的CPU老是在空轉,幹吃飯不幹活。因此咱們提出了操做數前推這樣的解決方案。

add $t0, $s2,$s1
add $s2, $s1,$t0

第一條指令,把 s1 和 s2 寄存器裏面的數據相加,存入到 t0 這個寄存器裏面。
第二條指令,把 s1 和 t0 寄存器裏面的數據相加,存入到 s2 這個寄存器裏面。

咱們要在第二條指令的譯碼階段以後,插入對應的NOP指令,直到前一天指令的數據寫回完成以後,才能繼續執行。可是這樣浪費了兩個時鐘週期。

這個時候徹底能夠在第一條指令的執行階段完成以後,直接將結果數據傳輸給到下一條指令的ALU。而後,下一條指令不須要再插入兩個NOP階段,就能夠繼續正常走到執行階段。

這樣的解決方案,咱們就叫做操做數前推(Operand Forwarding),或者操做數旁路(Operand Bypassing)。

CPU指令亂序執行

  1. 在取指令和指令譯碼的時候,亂序執行的CPU和其餘使用流水線架構的CPU是同樣的。它會一級一級順序地進行取指令和指令譯碼的工做。
  2. 在指令譯碼完成以後,CPU不會直接進行指令執行,而是進行一次指令分發,把指令發到一個叫做保留站(Reservation Stations)的地方。
  3. 這些指令不會馬上執行,而要等待它們所依賴的數據,傳遞給它們以後纔會執行。
  4. 一旦指令依賴的數據來齊了,指令就能夠交到後面的功能單元(Function Unit,FU),其實就是ALU,去執行了。咱們有不少功能單元能夠並行運行,可是不一樣的功能單元可以支持執行的指令並不相同。
  5. 指令執行的階段完成以後,咱們並不能馬上把結果寫回到寄存器裏面去,而是把結果再存放到一個叫做重排序緩衝區(Re-Order Buffer,ROB)的地方。
  6. 在重排序緩衝區裏,咱們的CPU會按照取指令的順序,對指令的計算結果從新排序。只有排在前面的指令都已經完成了,纔會提交指令,完成整個指令的運算結果。
  7. 實際的指令的計算結果數據,並非直接寫到內存或者高速緩存裏,而是先寫入存儲緩衝區(Store Buffer面,最終纔會寫入到高速緩存和內存裏。

在亂序執行的狀況下,只有CPU內部指令的執行層面,多是「亂序」的。

例子:

a = b + c
d = a * e
x = y * z

裏面的 d 依賴於 a 的計算結果,不會在 a 的計算完成以前執行。可是咱們的CPU並不會閒着,由於 x = y * z 的指令一樣會被分發到保留站裏。由於 x 所依賴的 y 和 z 的數據是準備好的, 這裏的乘法運算不會等待計算 d,而會先去計算 x 的值。

若是咱們只有一個FU可以計算乘法,那麼這個FU並不會由於 d 要等待 a 的計算結果,而被閒置,而是會先被拿去計算 x。

在 x 計算完成以後,d 也等來了 a 的計算結果。這個時候,咱們的FU就會去計算出 d 的結果。而後在重排序緩衝區裏,把對應的計算結果的提交順序,仍然設置成 a -> d -> x,而計算完成的順序是 x -> a -> d。

在這整個過程當中,整個計算乘法的FU都沒有閒置,這也意味着咱們的CPU的吞吐率最大化了。

亂序執行,極大地提升了CPU的運行效率。核心緣由是,現代CPU的運行速度比訪問主內存的速度要快不少。若是徹底採用順序執行的方式,不少時間都會浪費在前面指令等待獲取內存數據的時間裏。CPU不得不加入NOP操做進行空轉。

相關文章
相關標籤/搜索