[組成原理] 流水線設計

1. 時鐘週期、機器週期、指令週期

時鐘週期是由晶振決定的,晶振的一次震盪我們就叫做時鐘週期。我們平時說的處理器的主頻就可以理解爲單位時間內執行的簡單指令,例如3.8GHz主頻就是一秒種可以執行簡單指令3.8G條。

機器週期,也叫CPU週期。由於CPU運行很快,但是訪問內存的速度相對來說就慢太多了,所以我們一般把從內存中讀取一條指令的最短時間稱爲機器週期。

計算機每執行一條指令,都要經過取指令、指令譯碼、執行指令三個動作,這一系列動作我們就稱爲指令週期。

2. 單指令週期處理器

CPU執行指令的過程其實是分工協作的。取指令需要譯碼器把數據從內存取出,寫入寄存器。指令譯碼需要另外的譯碼器,把指令解析成對應的控制信號、內存地址和數據。執行指令需要ALU完成計算工作。

處理器在一個時鐘週期處理一整條指令,就稱爲單指令週期處理器。

由於不同指令的執行時間不同,爲了讓所有指令都在一個時鐘週期內完成,就要讓讓執行快的指令遷就一下執行慢的指令,也就是把時鐘週期設置成執行時間最長的那個指令的執行時間。也就是說,執行快的指令執行完後,我們必須等待滿一個時鐘週期後才能指令下一條指令。

3. 流水線設計

單指令週期處理器,在某一個元器件工作的時候,其他的元器件就要等着,就太浪費資源了。

流水線設計就是在當前指令的某一階段完成後,不需要等待整條指令全部完成,而是轉而執行下一條指令的同一階段。這樣的設計就可以提高處理器主頻,也就是縮短時鐘週期。因爲之前一條指令爲一個時鐘週期,現在一個階段就可以設爲一個時鐘週期了。

同樣,對比單指令週期處理器,我們需要找到最費時間的階段,將其設置爲時鐘週期,來確保每一個階段都可以在一個時鐘週期內完成。如果某一個階段的時間太長,明顯會影響處理速度,因爲其他階段執行完之後都在等他,這時我們會考慮將這個階段拆分成更小的階段。現代的 ARM 或者 Intel 的 CPU,流水線級數都已經到了 14 級,也就是將一條指令拆成了14個階段。

在這裏插入圖片描述

4. 結構冒險

結構冒險就是在兩條指令的不同階段都使用了同一個硬件,例如在訪存和取指令的時候,都需要訪問內存。
在這裏插入圖片描述

引入緩存

哈佛架構把內存分成了兩部分,數據內存和程序內存,但是對於程序指令和數據需要的內存空間,我們就沒有辦法根據實際的應用去動態分配了,雖然解決了資源衝突,但是失去了內存分配的靈活性。

現代的CPU借鑑了哈佛架構,將CPU內部的高速緩衝區分成了兩部分,這樣就解決了資源衝突的問題。

在這裏插入圖片描述

5. 數據冒險

數據冒險就是在執行的多條指令中,存在數據依賴的問題。學習過併發的同學一定有所瞭解。主要有三種,寫後讀,讀後寫,寫後寫。

1) 寫後讀

public static void main(String[] args) {
	  int a = 1;
	  int b = 2;
	  a = a + 2;
    // 1: 83 45 fc 02             add    DWORD PTR [rbp-0x4],0x2 
	  b = a + 3; 
  	// 2: 8b 45 fc                mov    eax,DWORD PTR [rbp-0x4] 
  	// 83 c0 03                add    eax,0x3
  	// 89 45 f8                mov    DWORD PTR [rbp-0x8],eax
}

在2處,讀取rbp-0x4地址的值之前,一定要先執行1處,將加法結果寫回到rbp-0x4地址。

2) 讀後寫

public static void main(String[] args) {
  	int a = 1;
	  int b = 2;
	  a = b + a; 
  	// 1:   8b 45 f8                mov    eax,DWORD PTR [rbp-0x8]
  	// 2:   01 45 fc                add    DWORD PTR [rbp-0x4],eax
  	b = a + b; 
  	// 3:   8b 45 fc                mov    eax,DWORD PTR [rbp-0x4]
  	// 4:   01 45 f8                add    DWORD PTR [rbp-0x8],eax
}

在3處,讀取rbp-0x4地址的值到eax寄存器之前,一定要先將原來eax數據加到rbp-0x4地址的值中去,否則程序就出錯了

3) 寫後寫

public static void main(String[] args) {
  	int a = 1;
  	// 1:		c7 45 fc 01 00 00 00    mov    DWORD PTR [rbp-0x4],0x1
  	a = 2;
  	// 2:   c7 45 fc 02 00 00 00    mov    DWORD PTR [rbp-0x4],0x2
}

要保證2中的寫內存在1之前完成

4) 流水線停頓

解決數據冒險的方法很簡單。我們發現流水線設計的核心就是在上一條數據還在處理的時候,下一條數據就要開始執行了,打破了指令執行的嚴格順序性。

爲了計算結果的正確性,我們必須滿足指令的順序性。當我們發現兩條指令之間存在數據依賴的話,我們會讓指令停頓一個或多個時鐘週期。具體來說,就是在對應位置插入一個NOP操作,這個操作什麼都不做。

在這裏插入圖片描述

6. 控制冒險

當順序執行的時候,我們可以提前準備下一條指令的執行,但是如果遇到了分支、循環語句,程序就不是順序執行的了。這時候,就要等當前指令執行完,更新了 PC 寄存器之後,才能知道,是執行下一條指令,還是跳轉到另外一個內存地址,去取別的指令。如果不是順序執行,而要取別的指令執行,那麼之前的取指令、譯碼操作就白費了,還需要丟棄掉已經執行的部分。

1) 靜態分支預測

很簡單,我們假設指令一定是順序執行的,這樣就有50%的正確率。如果假設錯誤了,就丟棄已經執行的部分,重新執行正確的指令。

2) 動態分支預測

動態分支預測,就是根據之前條件跳轉的結果來進行預測。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章