【梳理】計算機組成與設計 第4章 處理器 第4節 指令級並行(內附文檔高清截圖)

配套教材:
Computer Organization and Design: The Hardware / Software Interface (5th Edition)
建議先修課程:數字邏輯電路、C / C++、彙編語言。
這是專業必修課《計算機組成原理》的複習指引。建議將本複習指導與博客中的《簡明操作系統原理》配合複習。
在本文的最後附有複習指導的高清截圖。需要掌握的概念在文檔截圖中以藍色標識,並用可讀性更好的字體顯示 Linux 命令和代碼。代碼部分語法高亮。
計算機組成原理不是語言課,本複習指導對用到的編程語言的語法的講解也不會很細緻。如果不知道代碼中的一些關鍵字、指令或函數的具體用法,你應該自行查找相關資料。


第四節 指令級並行

36、這裏只對相應內容簡單介紹。如果想要詳細學習,請參閱《計算機架構:量化方法》。
指令級並行(instruction-level parallelism,ILP)指的是在一個週期內同時執行多條指令。常用的方法有兩種:一種是增長流水線,其利弊已經在前面作了簡單分析。另一種是增加硬件單元的數量,即多發射(multiple issue)。
指令發射,指的是指令從譯碼階段進入執行階段。

37、多發射分爲靜態多發射(static multiple issue)和動態多發射(dynamic multiple issue)兩種。
靜態多發射高度依賴編譯器,由編譯器在編譯時,進行相關性分析和靜態分支預測,以靜態完成指令打包或冒險處理。
指令打包,指的是將同時發射的多條指令合併到一個長指令中。將一個週期內發射的多個指令看成一條多個操作的長指令,稱爲一個發射包(issue packet)。靜態多發射最初被稱爲超長指令字(VLIW,Very Long Instruction Word),採用這種技術的處理器被稱爲VLIW處理器。不過,在同一個週期內發射的指令類型是受限制的(例如,只能是一條ALU指令/分支指令、一條Load/Store指令),以降低解碼和指令發射的實現難度。
IA-64(其爲RISC)採用這種方法,Intel稱其爲EPIC(Explicitly Parallel Instruction Computer,顯式並行指令計算機)。
冒險處理,指的是減少或消除數據冒險和控制冒險。
做法1:完全由編譯器通過代碼調度和插入nop指令來消除所有冒險,無需硬件實現冒險檢測和流水線阻塞。
做法2:由編譯器通過靜態分支預測和代碼調度來消除同時發射指令間內部依賴,由硬件檢測數據冒險並進行流水線阻塞。
即:保證打包指令內部不會出現冒險。
動態多發射處理器由硬件在執行時動態完成指令打包或冒險處理,通常被稱爲超標量(Superscalar)處理器。針對VLIW處理器的編譯結果與機器結構密切相關,在結構有差異(即使指令集相同)的機器上要重新針對編譯。而對於超標量處理器,編譯器僅進行指令順序調整,但不進行指令打包,由硬件根據機器的結構來決定一個週期發射哪幾條指令。因此,編譯後的代碼能夠被不同結構的機器正確執行。

38、推測(speculation)基於預測(prediction)的思想,是實現指令打包和冒險處理的基礎。前面我們講過,通過分支預測可以推測分支結果。當分支條件成立的概率不大時,先執行緊隨分支指令之後的指令。另一個推測的例子就是假設跟在load指令之前的store指令的地址肯定與這個load指令的不同。於是在需要時,這個load指令可以在store之前執行。
既然是推測,就意味着有時候是錯的。任何推測機制都必須實現對推測正確性的檢測,並在推測錯誤後回滾(rollback / unroll)已經根據推測結果執行的指令。
編譯器可以通過推測機制來調整指令的執行順序。
軟件與硬件在推測錯誤後的恢復機制是不同的。編譯器一般通過插入額外指令來檢查推測結果是否正確,並提供負責恢復的指令。而CPU進行該過程時,先緩存推測結果。如推測正確,則將結果直接寫入寄存器或內存;反之,清空緩存並重新按正確的順序執行。
推測機制可能會引發原先沒有的異常。編譯器針對此種情況的方案是:忽略它們,除非該指令確實會被執行併發生異常。在基於硬件的推測中,異常也被暫時存入緩衝區,直到該指令確實會被執行時,才產生異常,並進入正常的異常處理程序。

39、以下是一個基於MIPS的靜態雙發射數據通路。

該通路一次讀出2條符合要求的指令:一條爲計算,一條爲存取(沒有配對指令時,填充空指令)。可以看出,多了兩個ALU,一個用於執行各種算術邏輯操作,另一個則專門計算新的PC值。帶符號擴展單元、寄存器堆的讀寫端口和內存訪問端口的數量也翻倍了。
雙發射有時可能會引發相對更多的性能損失。例如:如果在load指令後的一條指令需要用到讀取的數據,那麼這條指令與load指令之間會自動空出一個週期的延遲,稱爲使用延遲(use latency)。假設緊隨其後的下一對指令都需要用到剛纔讀取的數據,那麼這兩條指令就要一起停頓一個週期。顯然,如果CPU只有單發射,這兩條指令之間不用延遲,只需要排在前面的那條指令與load之間隔開1個週期即可。也就是說,執行同樣的這段指令,執行到這一處時,在雙發射CPU上要比單發射CPU上多了1週期的延遲,使得實際性能的提升幅度減小。爲了避免這種情形,在編譯器和硬件調度方面都要下更多的功夫。

40、以下是一段MIPS彙編指令:
Loop:
lw t0,0(t0, 0(s1) # $t0=array element
addu t0,t0,t0,$s2 # add scalar in $s2
sw t0,0(t0, 0(s1) # store result
addi s1,s1,s1,–4 # decrement pointer
bne s1,s1,zero,Loop # branch $s1!=0
由於前三條指令和後兩條指令都具有相關性,並且雙發射同時執行的指令類型有限制(一條ALU或分支指令,一條存取指令),最終只能這樣子安排指令的執行:

實際IPC僅爲1.25,而非理論上的最高值2。注意:計算IPC時,不計nop的影響。

41、循環展開(loop unrolling),指的是將循環中的一系列語句去掉控制循環的指令,並根據循環次數複製若干份副本。這循環展開可以將每兩輪循環之間的指令重疊起來執行,提升IPC。
針對上面的例子進行循環展開。我們把這段指令複製三個副本,與原來的指令加起來一共4份。

編譯器額外使用了一些寄存器,這稱爲寄存器重命名(register renaming),可以消除名稱依賴(name dependence)——實際上這並不是數據依賴。不過這個技巧並不是總帶來正面效果的,有時候它會帶來額外的流水線衝突或者使得編譯器無法更靈活地調整代碼順序。
經過循環展開後,IPC提升到了1.75,代價是更大的代碼佔用空間和寄存器佔用數量。

42、採用動態多發射的處理器,一般稱爲超標量處理器(superscalars)。這種處理器在程序運行期間決定哪些指令可以並行。當然,動態多發射處理器仍然需要編譯器配合:編譯器負責調整指令的順序,以減少依賴性。代碼的正確執行由硬件來保障。
多數超標量處理器都結合動態流水線調度(Dynamic pipeline scheduling)技術:通過指令相關性檢測和動態分支預測等手段,投機性地不按指令順序執行。發生流水線阻塞時,可以到後面找指令來執行。
一個採用動態流水線調度技術的CPU的流水線中的硬件單元可以分爲如下三種:取指令與解碼單元、功能單元和提交單元(commit unit)。第一個單元取指令並解碼,將指令發送至相應的功能單元。每個功能單元都有緩衝區,稱爲保留站(reservation station,也稱執行緩衝區),暫存操作數和操作。當功能單元準備就緒後,開始計算結果。得到結果後,送至保留站等待提交單元提交。當時機適宜後,提交單元正式將數據寫入目標寄存器或內存。提交單元的緩衝區常被稱爲重排序緩衝區(reorder buffer,ROB),被用於將調整了執行順序的指令重新按原有順序排列,稱爲有序提交(in-order commit)。

保留站和重排序緩衝爲寄存器重命名提供了實現機制。當指令發射後,它會被複制到正確的功能單元的保留站,寄存器堆和重排序緩衝中的可用操作數也會一併被複制進去。這些緩衝會一直保留到所有需要的操作數和功能單元都可用,緩衝內容在之後會被新的數據覆寫。如果寄存器堆和重排序緩衝中讀取不到數據,意味着需要的數據應當來自功能單元的執行結果。當相應的功能單元給出結果後,這個結果會不經過寄存器,直接寫入保留站。動態流水線調度使得指令的執行順序與取指令的順序不同,這種執行方式也稱爲亂序執行(out-of-order execution,OoOE)。雖然取指令和提交是按順序執行的,但中間的過程可以靈活調整執行順序,提升性能。

43、動態流水線調度常與基於硬件的推測結合,包括基於硬件的分支預測。在指令提交前,常常就得以知曉哪些預測是錯誤的,並令這些錯誤執行不提交。動態調度的流水線還可以對將要讀取的內存地址預測,將存取指令重排,通過拒絕因爲預測錯誤而執行的指令被提交來減少性能損失。

44、前端主要包括取指令、解碼和分支預測;後端則由保留站、亂序執行及其控制單元、提交單元構成。前端和後端之間還有指令控制器用來把前端解碼出來的操作分發指令給執行單元。

45、編譯器可以依據數據依賴關係來調度代碼,爲什麼還要超標量處理器來動態調度?原因主要有三:
(1)並不是所有停頓都可以事先發現。例如緩存缺失(將在第5章學習)無法被編譯器提前預知,因爲程序開始執行之前,緩存的使用情況顯然是不唯一的。動態調度在運行期發現這類阻塞後,可以提前執行無關指令。
(2)當CPU採用動態分支預測時,編譯器無法提前得知指令的最終執行順序,需要根據執行的實際情況進行預測。
(3)發射寬度和流水線延遲在同一指令集的不同CPU上亦可不同,流水線的結構也會影響循環展開的深度和寄存器重命名的效果。於是最佳的指令順序也會改變。通過動態調度使得處理器細節被屏蔽起來,軟件發行商無需針對同一指令集的不同處理器發行相應的編譯器,並且以前的代碼也可在新的處理器上運行,無需重新編譯。

46、現代處理器雖然普遍達到三發射、四發射,但只有很少程序實際運行時能夠在每週期執行超過兩條指令。
首先,流水線內的主要性能瓶頸是無法被消除的指令間依賴。有些依賴是無法消除的,也有一些是編譯器和硬件無法判定爲依賴的。爲了保證指令執行的正確性,只好保守地調整。例如:使用指針的方法,尤其是可能導致別名的指針用法,可能會產生更多的潛在依賴。相比之下,對長數組的循環遍歷一般就沒有依賴。有的分支比較複雜,預測並不好做。
其次就是內存訪問的異常或者內存方面的系統調用,這些停頓也有無法消除的。

47、大量事實告訴我們,具有亂序執行和激進的推測機制的複雜CPU在能耗比上往往比順序執行的簡單CPU更差。所以近年來的CPU開始降低流水線級數,並在推測機制上針對能效優化。

48、提交單元負責將結果寫回寄存器堆或內存,不過有的CPU會在執行時率先更新寄存器堆。一些CPU會用額外的寄存器實現寄存器重命名,並且寄存器原有的內容仍會被保存,直到得以確定推測結果是否正確。

49、動態流水線可以分爲三種執行模式:按序發射按序完成、按序發射無序完成和無序發射無序完成。
最保守的方案是順序完成(順序提交),好處有:
(1) 簡化異常檢測和異常處理。
(2) 能在被推測指令完成前得知推測結果的正確性。
按序發射,指的是執行單元有空,上一條已發射就執行;無序發射,指的是執行單元一有空就執行。按序提交,指的是寫回單元有空,可在上一條已經寫回後寫回,或與上一條同時寫回;無序提交,指的是寫回單元一有空就寫回。
無序發射的超標量CPU中,譯碼後的指令被存放在一個叫做指令窗口的緩衝器中,等待發射。當所需功能部件可用、且無衝突或無相關性阻礙指令執行時,就從指令窗口發射,與取指和譯碼的順序無關。在無序發射和無需完成中,無關指令可以先行發射和先行完成。

50、微架構(microarchitecture),是指一個處理器的內部設計與結構,包括功能單元、緩存、寄存器堆、指令發射、流水線控制和互連(interconnect)等。

51、回憶之前進行DGEMM的代碼:
void dgemm(int n, double *A, double *B, double C) {
for (int i = 0; i < n; i += 4)
for (int j = 0; j < n; ++j) {
__m256d c0 = _mm256_load_pd(C + i + j * n); /
c0 = C[i][j] /
for (int k = 0; k < n; ++k)
c0 = _mm256_add_pd(c0, /
c0 += A[i][k]*B[k][j] /
_mm256_mul_pd(_mm256_load_pd(A + i + k * n),
_mm256_broadcast_sd(B + k + j * n)));
_mm256_store_pd(C + i + j * n, c0); /
C[i][j] = c0 */
}
}
這段代碼產生的彙編指令如下:

  1. vmovapd (%r11),%ymm0 ; Load 4 elements of C into %ymm0
  2. mov %rbx,%rcx ; register %rcx = %rbx
  3. xor %eax,%eax ; register %eax = 0
  4. vbroadcastsd (%rax,%r8,1),%ymm1 ; Make 4 copies of B element
  5. add $0x8,%rax ; register %rax = %rax + 8
  6. vmulpd (%rcx),%ymm1,%ymm1 ; Parallel mul %ymm1,4 A elements
  7. add %r9,%rcx ; register %rcx = %rcx + %r9
  8. cmp %r10,%rax ; compare %r10 to %rax
  9. vaddpd %ymm1,%ymm0,%ymm0 ; Parallel add %ymm1, %ymm0
  10. jne 50 <dgemm+0x50> ; jump if not %r10 != %rax
  11. add $0x1,%esi ; register % esi = % esi + 1
  12. vmovapd %ymm0,(%r11) ; Store %ymm0 into 4 C elements
    現在有一個啓用循環展開優化的版本:
    #include <intrin.h>
    #define UNROLL (4)

void dgemm(int n, double* A, double* B, double* C) {
for (int i = 0; i < n; i += UNROLL * 4)
for (int j = 0; j < n; ++j) {
__m256d c[4];
for (int x = 0; x < UNROLL; ++x)
c[x] = _mm256_load_pd(C + i + x * 4 + j * n);
for (int k = 0; k < n; ++k) {
__m256d b = _mm256_broadcast_sd(B + k + j * n);
for (int x = 0; x < UNROLL; ++x)
c[x] = _mm256_add_pd(
c[x], _mm256_mul_pd(
_mm256_load_pd(A + n * k + x * 4 + i), b));
}
for (int x = 0; x < UNROLL; ++x)
_mm256_store_pd(C + i + x * 4 + j * n, c[x]);
}
}
這段代碼產生的彙編指令如下:
1 vmovapd (%r11),%ymm4 ; Load 4 elements of C into %ymm4
2 mov %rbx,%rax ; register %rax = %rbx
3 xor %ecx,%ecx ; register %ecx = 0
4 vmovapd 0x20(%r11),%ymm3 ; Load 4 elements of C into %ymm3
5 vmovapd 0x40(%r11),%ymm2 ; Load 4 elements of C into %ymm2
6 vmovapd 0x60(%r11),%ymm1 ; Load 4 elements of C into %ymm1
7 vbroadcastsd (%rcx,%r9,1),%ymm0 ; Make 4 copies of B element
8 add $0x8,%rcx ; register %rcx = %rcx + 8
9 vmulpd (%rax),%ymm0,%ymm5 ; Parallel mul %ymm1,4 A elements
10 vaddpd %ymm5,%ymm4,%ymm4 ; Parallel add %ymm5, %ymm4
11 vmulpd 0x20(%rax),%ymm0,%ymm5 ; Parallel mul %ymm1,4 A elements
12 vaddpd %ymm5,%ymm3,%ymm3 ; Parallel add %ymm5, %ymm3
13 vmulpd 0x40(%rax),%ymm0,%ymm5 ; Parallel mul %ymm1,4 A elements
14 vmulpd 0x60(%rax),%ymm0,%ymm0 ; Parallel mul %ymm1,4 A elements
15 add %r8,%rax ; register %rax = %rax + %r8
16 cmp %r10,%rcx ; compare %r8 to %rax
17 vaddpd %ymm5,%ymm2,%ymm2 ; Parallel add %ymm5, %ymm2
18 vaddpd %ymm0,%ymm1,%ymm1 ; Parallel add %ymm0, %ymm1
19 jne 68 <dgemm+0x68> ; jump if not %r8 != %rax
20 add $0x1,%esi ; register % esi = % esi + 1
21 vmovapd %ymm4,(%r11) ; Store %ymm4 into 4 C elements
22 vmovapd %ymm3,0x20(%r11) ; Store %ymm3 into 4 C elements
23 vmovapd %ymm2,0x40(%r11) ; Store %ymm2 into 4 C elements
24 vmovapd %ymm1,0x60(%r11) ; Store %ymm1 into 4 C elements
測試條件:Sandy Bridge Core i7,矩陣大小:32×32。
編譯條件:循環展開版本爲-O3,且展開深度爲4。
測試結果:
Freq (GHz) 2.6 3.3 2.6 3.3 2.6 3.3
Version Unoptimized Unoptimized AVX AVX AVX, DLP, ILP AVX, DLP, ILP
GFLOPS 1.7 2.1 6.4 8.1 14.6 18.6
注:3.3 GHz爲Turbo開啓的頻率,2.6 GHz爲Turbo關閉的頻率。

52、實現流水線設計並不容易。本複習指導的配套教材《計算機組成與設計:硬件 / 軟件接口》的兩位作者(David A. Patterson,John L. Hennessy)寫過另一本更爲深入的體系結構書《計算機架構:量化方法》。該書的第一版(現在已經更新到第11版)出版時,講解流水線的內容中有一個bug,即使經過了100人的複查,並且在18所大學的課上使用過,也未能將其發現,直到有人嘗試按照書本的內容去實現這樣的一個CPU。使用Verilog來實現Core i7的流水線至少需要幾千行。

53、不適當的指令集設計會對流水線產生不利影響。
變化的指令長度和運行時間會導致流水線階段的不平衡,嚴重影響流水線衝突的探測。直到1980年的DEC VAX 8500開始,這個問題才通過引入微操作和微流水線(micropipeline)機制成功解決。這兩個機制也是Intel Core i7一直使用到如今的機制。當然,指令與微指令之間的翻譯和一致性保持還是需要一定的開銷。
複雜的尋址模式會帶來一系列問題。需要更新寄存器的尋址模式可能使流水線衝突的檢測變得困難。一些需要多次內存訪問的尋址模式則給流水線控制帶來了極大壓力,導致很難保持流水線的充分負載。
DEC Alpha和DEC NVAX是一對很好的例子。Alpha採用了新的ISA,使得其性能達到NVAX的兩倍以上。另一個例子是MIPS M/2000和DEC VAX 8700。雖然M/2000在SPEC基準測試中執行了更多的指令數,但是VAX 8700運行SPEC項目時平均執行的週期數達到了M/2000的2.7倍,導致最終MIPS M/2000的速度更快。

在這裏插入圖片描述在這裏插入圖片描述在這裏插入圖片描述在這裏插入圖片描述在這裏插入圖片描述在這裏插入圖片描述在這裏插入圖片描述在這裏插入圖片描述在這裏插入圖片描述在這裏插入圖片描述在這裏插入圖片描述在這裏插入圖片描述在這裏插入圖片描述在這裏插入圖片描述在這裏插入圖片描述在這裏插入圖片描述在這裏插入圖片描述在這裏插入圖片描述在這裏插入圖片描述

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