深入淺出計算機組成原理學習筆記:第二十二講


一、引子

1、流水線設計需要解決的三大冒險

過去兩講,我為你講解了流水線設計CPU所需要的基本概念。接下來,我們一起來看看,要想通過流水線設計來提升CPU的吞吐率,我們需要冒哪些風險。

任何一本講解CPU的流水線設計的教科書,都會提到流水線設計需要解決的三大冒險,分別是 結構冒險(Structural Harzard)、 數據冒險(Data Harzard)以及 控制冒險(Control Harzard)。

2、為什么在流水線設計里,harzard沒有翻譯成“危機”,而是要叫“冒險”呢?

這三大冒險的名字很有意思,它們都叫作 harzard(冒險)。喜歡玩游戲的話,你應該知道一個著名的游戲,生化危機,英文名就叫Bioharzard。的確,harzard還有一個意思就是“危機”。
那為什么在流水線設計里,harzard沒有翻譯成“危機”,而是要叫“冒險”呢?

在CPU的流水線設計里,固然我們會遇到各種“危險”情況,使得流水線里的下一條指令不能正常運行。但是,我們其實還是通過“搶跑”的方式,“冒險”拿到了一個提升指令吞吐率的機會。
流水線架構的CPU,是我們主動進行的冒險選擇。我們期望能夠通過冒險帶來更高的回報,所以,這不是無奈之下的應對之舉,自然也算不上什么危機了。

事實上,對於各種冒險可能造成的問題,我們其實都准備好了應對的方案。這一講里,我們先從結構冒險和數據冒險說起,一起來看看這些冒險及其對應的應對方案。

二、結構冒險:為什么工程師都喜歡用機械鍵盤?

我們先來看一看結構冒險。結構冒險,本質上是一個硬件層面的資源競爭問題,也就是一個硬件電路層面的問題。CPU在同一個時鍾周期,同時在運行兩條計算機指令的不同階段。但是這兩個不同的階段,可能會用到同樣的硬件電路。最典型的例子就是內存的數據訪問。請你看看下面這張示意圖,其實就是第20講里對應的5級流水線的示意圖。

1、沒辦法同時執行第1條指令的讀取內存數據和第4條指令的讀取指令代碼

可以看到,在第1條指令執行到訪存(MEM)階段的時候,流水線里的第4條指令,在執行取指令(Fetch)的操作。訪存和取指令,都要進行內存數據的讀取。我們的內存,
只有一個地址譯碼器的作為地址輸入,那就只能在一個時鍾周期里面讀取一條數據,沒辦法同時執行第1條指令的讀取內存數據和第4條指令的讀取指令代碼

 

 

2、廉價的薄膜鍵盤共用一個線路

類似的資源沖突,其實你在日常使用計算機的時候也會遇到。最常見的就是薄膜鍵盤的“鎖鍵”問題。常用的最廉價的薄膜鍵盤,並不是每一個按鍵的背后都有一根獨立的線路,
而是多個鍵共用一個線路。如果我們在同一時間,按下兩個共用一個線路的按鍵,這兩個按鍵的信號就沒辦法都傳輸出去。

3、這也是為什么,重度鍵盤用戶,都要買貴一點兒的機械鍵盤或者電容鍵盤

這也是為什么,重度鍵盤用戶,都要買貴一點兒的機械鍵盤或者電容鍵盤。因為這些鍵盤的每個按鍵都有獨立的傳輸線路,可以做到“全鍵無沖”,這樣,無論你是要大量寫文章、寫程序,
還是打游戲,都不會遇到按下了鍵卻沒生效的情況。

4、全鍵無沖”這樣的資源沖突解決方案,其實本質就是 增加資源

“全鍵無沖”這樣的資源沖突解決方案,其實本質就是 增加資源。同樣的方案,我們一樣可以用在CPU的結構冒險里面。對於訪問內存數據和取指令的沖突,一個直觀的解決方案就是把我們的內存分成兩部分,讓它們各有各的地址譯碼器。這兩部分分別是 存放指令的程序內存和存放數據內存

這樣把內存拆成兩部分的解決方案,在計算機體系結構里叫作哈佛架構(Harvard Architecture),來自哈佛大學設計Mark I型計算機時候的設計。對應的,我們之前說的馮·諾依曼體系結構,
又叫作普林斯頓架構(Princeton Architecture)。從這些名字里,我們可以看到,早年的計算機體系結構的設計,其實產生於美國各個高校之間的競爭中。

5、我們今天使用的CPU並沒有把內存拆成程序內存和數據內存這兩部份

不過,我們今天使用的CPU,仍然是馮·諾依曼體系結構的,並沒有把內存拆成程序內存和數據內存這兩部分。因為如果那樣拆的話,對程序指令和數據需要的內存空間,
我們就沒有辦法根據實際的應用去動態分配了。雖然解決了資源沖突的問題,但是也失去了靈活性。

 

 

不過,借鑒了哈佛結構的思路,現代的CPU雖然沒有在內存層面進行對應的拆分,卻在CPU內部的高速緩存部分進行了區分,把高速緩存分成了 指令緩存(Instruction Cache)和 數據緩存(Data Cache)兩部分。

內存的訪問速度遠比CPU的速度要慢,所以現代的CPU並不會直接讀取主內存。它會從主內存把指令和數據加載到高速緩存中,這樣后續的訪問都是訪問高速緩存。而指令緩存和數據緩存的拆分,使得我們的CPU在進行數據訪問和取指令的時候,不會再發生資源沖突的問題了。

三、數據冒險:三種不同的依賴關系

結構冒險是一個硬件層面的問題,我們可以靠增加硬件資源的方式來解決。然而還有很多冒險問題,是程序邏輯層面的事兒。其中,最常見的就是數據冒險。

數據冒險,其實就是同時在執行的多個指令之間,有數據依賴的情況。這些數據依賴,我們可以分成三大類,分別是 先寫后讀(Read After Write,RAW)、 先讀后寫(Write After Read,WAR)和 寫后再寫(Write After Write,WAW)。下面,我們分別看一下這幾種情況。

1、先寫后讀(Read After Write)

我們先來一起看看先寫后讀這種情況。這里有一段簡單的C語言代碼編譯出來的匯編指令。這段代碼簡單地定義兩個變量 a 和 b,然后計算 a = a + 2。再根據計算出來的結果,計算 b = a + 3。

代碼

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

匯編

int main() {
   0:   55                      push   rbp
   1:   48 89 e5                mov    rbp,rsp
  int a = 1;
   4:   c7 45 fc 01 00 00 00    mov    DWORD PTR [rbp-0x4],0x1
  int b = 2;
   b:   c7 45 f8 02 00 00 00    mov    DWORD PTR [rbp-0x8],0x2
  a = a + 2;
  12:   83 45 fc 02             add    DWORD PTR [rbp-0x4],0x2
  b = a + 3;
  16:   8b 45 fc                mov    eax,DWORD PTR [rbp-0x4]
  19:   83 c0 03                add    eax,0x3
  1c:   89 45 f8                mov    DWORD PTR [rbp-0x8],eax
}
  1f:   5d                      pop    rbp
  20:   c3                      ret  

你可以看到,在內存地址為12的機器碼,我們把0x2添加到 rbp-0x4 對應的內存地址里面。然后,在緊接着的內存地址為16的機器碼,我們又要從rbp-0x4這個內存地址里面,把數據寫入到eax這個寄存器里面。

所以,我們需要保證,在內存地址為16的指令讀取rbp-0x4里面的值之前,內存地址12的指令寫入到rbp-0x4的操作必須完成。這就是先寫后讀所面臨的數據依賴。如果這個順序保證不了,我們的程序就會出錯。

這個先寫后讀的依賴關系,我們一般被稱之為 數據依賴,也就是Data Dependency。

2、先讀后寫(Write After Read)

我們還會面臨的另外一種情況,先讀后寫。我們小小地修改一下代碼,先計算 a = b + a,然后再計算 b = a+ b。

代碼

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

匯編

int main() {
   0:   55                      push   rbp
   1:   48 89 e5                mov    rbp,rsp
   int a = 1;
   4:   c7 45 fc 01 00 00 00    mov    DWORD PTR [rbp-0x4],0x1
   int b = 2;
   b:   c7 45 f8 02 00 00 00    mov    DWORD PTR [rbp-0x8],0x2
   a = b + a;
  12:   8b 45 f8                mov    eax,DWORD PTR [rbp-0x8]
  15:   01 45 fc                add    DWORD PTR [rbp-0x4],eax
   b = a + b;
  18:   8b 45 fc                mov    eax,DWORD PTR [rbp-0x4]
  1b:   01 45 f8                add    DWORD PTR [rbp-0x8],eax
}
  1e:   5d                      pop    rbp
  1f:   c3                      ret       

我們同樣看看對應生成的匯編代碼。在內存地址為15的匯編指令里,我們要把 eax 寄存器里面的值讀出來,再加到 rbp-0x4 的內存地址里。接着在內存地址為18的匯編指令里,
我們要再寫入更新 eax 寄存器里面。

如果我們在內存地址18的eax的寫入先完成了,在內存地址為15的代碼里面取出 eax 才發生,我們的程序計算就會出錯。這里,我們同樣要保障對於eax的先讀后寫的操作順序。

這個先讀后寫的依賴,一般被叫作 反依賴,也就是Anti-Dependency。

3、寫后再寫(Write After Write)

我們再次小小地改寫上面的代碼。這次,我們先設置變量 a = 1,然后再設置變量 a = 2。

代碼

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

匯編

int main() {
   0:   55                      push   rbp
   1:   48 89 e5                mov    rbp,rsp
  int a = 1;
   4:   c7 45 fc 01 00 00 00    mov    DWORD PTR [rbp-0x4],0x1
  a = 2;
   b:   c7 45 fc 02 00 00 00    mov    DWORD PTR [rbp-0x4],0x2
}

在這個情況下,你會看到,內存地址4所在的指令和內存地址b所在的指令,都是將對應的數據寫入到 rbp-0x4 的內存地址里面。如果內存地址b的指令在內存地址4的指令之后寫入。
那么這些指令完成之后,rbp-0x4 里的數據就是錯誤的。這就會導致后續需要使用這個內存地址里的數據指令,沒有辦法拿到正確的值。

所以,我們也需要保障內存地址4的指令的寫入,在內存地址b的指令的寫入之前完成。

這個寫后再寫的依賴,一般被叫作 輸出依賴,也就是Output Dependency。

四、再等等:通過流水線停頓解決數據冒險

除了讀之后再進行讀,你會發現,對於同一個寄存器或者內存地址的操作,都有明確強制的順序要求。而這個順序操作的要求,也為我們使用流水線帶來了很大的挑戰。
因為流水線架構的核心,就是在前一個指令還沒有結束的時候,后面的指令就要開始執行。

所以,我們需要有解決這些數據冒險的辦法。其中最簡單的一個辦法,不過也是最笨的一個辦法,就是流水線停頓(Pipeline Stall),或者叫流水線冒泡(Pipeline Bubbling)。

流水線停頓的辦法很容易理解。如果我們發現了后面執行的指令,會對前面執行的指令有數據層面的依賴關系,那最簡單的辦法就是“ 再等等”。我們在進行指令譯碼的時候,
會拿到對應指令所需要訪問的寄存器和內存地址。所以,在這個時候,我們能夠判斷出來,這個指令是否會觸發數據冒險。如果會觸發數據冒險,

我們就可以決定,讓整個流水線停頓一個或者多個周期。

 

 

我在前面說過,時鍾信號會不停地在0和1之前自動切換。其實,我們並沒有辦法真的停頓下來。流水線的每一個操作步驟必須要干點兒事情。所以,在實踐過程中,
我們並不是讓流水線停下來,而是在執行后面的操作步驟前面,插入一個NOP操作,也就是執行一個其實什么都不干的操作。

 

 

這個插入的指令,就好像一個水管(Pipeline)里面,進了一個空的氣泡。在水流經過的時候,沒有傳送水到下一個步驟,而是給了一個什么都沒有的空氣泡。這也是為什么,我們的流水線停頓,
又被叫作流水線冒泡(Pipeline Bubble)的原因。

五、總結延伸

講到這里,相信你已經弄明白了什么是結構冒險,什么是數據冒險,以及數據冒險所要保障的三種依賴,也就是數據依賴、反依賴以及輸出依賴。

一方面,我們可以通過增加資源來解決結構冒險問題。我們現代的CPU的體系結構,其實也是在馮·諾依曼體系結構下,借鑒哈佛結構的一個混合結構的解決方案。
我們的內存雖然沒有按照功能拆分,但是在高速緩存層面進行了拆分,也就是拆分成指令緩存和數據緩存這樣的方式,從硬件層面,使得同一個時鍾下對於相同資源的競爭不再發生。

另一方面,我們也可以通過“等待”,也就是插入無效的NOP操作的方式,來解決冒險問題。這就是所謂的流水線停頓。不過,流水線停頓這樣的解決方案,是以犧牲CPU性能為代價的。
因為,實際上在最差的情況下,我們的流水線架構的CPU,又會退化成單指令周期的CPU了。

所以,下一講,我們進一步看看,其他更高級的解決數據冒險的方案,以及控制冒險的解決方案,也就是操作數前推、亂序執行和還有分支預測技術。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM