banner
Alex Wu

Alex Wu

college student in CS | web3 | Gopher | lifelong grower | cat lover https://wureny.xyz/
github
x
email

CPU性能提高术

概述#

CPU 性能的兩個指標:響應時間和吞吐率;

本篇文章將著重介紹以下幾點:

  1. 流水線技術
  2. 利用好流水線技術裡遇到的三大冒險
  3. 亂序執行
  4. 超標量技術提高吞吐率

流水線技術#

一條 CPU 指令的執行過程可以簡單劃分為以下三步:

  1. 取指令
  2. 指令譯碼
  3. 指令執行

由於取指令時需要依靠時鐘信號來讓 PC 寄存器自增,所以一條指令的完成至少需要一個時鐘周期;

按照最初的設計,我們希望性能最好,於是我們需要在一個時鐘周期內完成且僅完成一條指令的處理,因為每一條指令的複雜度是不一樣的,而時鐘周期是固定的,所以時鐘周期的長度必須達到執行時間最久的那一條指令;

這種設計的缺陷也很明顯:非常浪費時間和資源;

不論我們運行的指令複雜還是簡單,都必須要等滿一整個時鐘周期後才能運行下一條指令,而在這期間,CPU 很多資源,比如 ALU,很可能長時間處於閒置狀態,造成資源的浪費;

至此,CPU 需要一種技術,能夠使其盡可能地避免運行指令時時間與 CPU 硬體資源浪費的問題;

現代 CPU 往往採用流水線技術來解決這個問題;

簡單來說,流水線是一種實現多條指令重疊執行的過程,我們將一個任務拆分成幾個步驟,一條流水線能夠處理一條指令的各個步驟,而同一時刻,一個任務只會處於一個步驟,在第一個任務進入流水線,並從步驟 1 到達步驟 2 後,流水線處理步驟 1 的部分就空出來了,第二個任務就可以進入流水線進行步驟 1,而不必等到第一個任務完全結束再進入;

具體到指令的流水線化,需要定義流水線級別數,也就是上文所說的步驟數;

不同 CPU 有不同的級別數,有的性能敏感分地精細,有十幾層,有的則只有幾層;

我們以 ARMv8 的子指令集 —LEGv8 為例,劃分為五個處理步驟:

  1. 從指令寄存器中取指令
  2. 讀取寄存器並通過地址譯碼器譯碼
  3. 執行指令
  4. 讀寫內存
  5. 寫回寄存器

每一個流水線級別的操作,都需要佔用一個時鐘周期,一條指令最終需要流水線級別數個時鐘周期;

時鐘周期長度也不再需要達到一條指令運行完的時間,只需要達到最複雜的流水線級別的操作的時長即可;

與此同時,單條指令的執行時間並沒有因為流水線技術而得到優化,流水線是通過增加指令的吞吐率來提高性能的;

此外,流水線級別數也不能過多,因為流水線深度的增加是有成本的;

每一級流水線的輸出,都需要保存到額外的流水線寄存器中,雖然流水線寄存器讀寫速度很快,但一旦層級數目多了起來,比如達到三四十,那也會降低響應的時間,而且還會增大功耗。

流水線三大冒險#

流水線會出現這樣一種情況:在下一個時鐘周期中下一條指令無法執行,我們將這種情況稱為冒險(hazard);

所謂冒險,雖遇危機,但也必然有克服危機後的收穫,在流水線技術中的冒險,假使我們好好解決遇到的問題,那能使 CPU 的吞吐率再上一個台階;

流水線技術中會遇到三種冒險:結構冒險,數據冒險,控制冒險;

結構冒險#

結構冒險的本質,是因硬體資源不足而造成的資源競爭問題;

在流水線中,處於不同階段的兩個指令假如都需要同樣的電路資源,而這一種電路資源又恰好只有一份,這時就會發生結構冒險,使得指令無法順利運行;

以上文劃分的那五個步驟為例,第二步和第四步都需要用到地址譯碼器,由於只有一個地址譯碼器,取指令地址和取數據所在內存的地址會發生資源衝突;

解決辦法有兩個;

一個是將內存拆分為兩部分,一部分放數據,一部分放指令,各自擁有各自的譯碼器,缺陷很明顯:由於內存一分為二,沒法動態分配,失去了靈活性;

還有一個,是將 CPU 內部的高速緩存分為指令緩存和數據緩存兩部分,這樣既可以解決結構冒險中的資源衝突問題,也不妨礙內存的動態分配;

前一種設計叫做哈佛架構,而後一種設計則被現代的冯・诺依曼體系結構所採用;

數據冒險#

定義為:當一個步驟必須等待另外一個步驟完成才能進行(即存在數據依賴),此時將產生流水線的停頓,也就是數據冒險;

比如以下這段代碼:

a :=1
b :=2
a = a+1
b = a+1

變量 b 的最終值依賴於變量 a 在執行完 add 1 之後的結果,這個順序必須要保證;

最簡單的方案是通過 ** 流水線冒泡,** 即:在譯碼完成之後,對該指令判斷是否存在數據依賴,如果存在,則通過插入 NOP 操作(代表了什麼都不幹)的方式來達到流水線停頓,等到數據依賴問題解決了的那個時鐘周期再開始正式執行(流水線是依靠時鐘信號進行工作的,不可能真的停下來,所以需要依靠這個 NOP 操作);此方案對 CPU 性能消耗比較大;

一個更為高級的策略是操作數前推,或者叫做操作數旁路;

我們以下面兩條指令為例:

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

第二條指令的執行依賴於 t0 中的值,按照之前的流水線冒泡策略,我們必須等待第一條指令的執行結果寫回 t0 後才能運行;但事實上,第二條指令完全無需等待 s2+s1 的結果寫回 t0 後再執行,s1+s2 的結果可以直接傳給第二條指令作為 t0 的輸入,只要在電路中增加增加一些線路出來即可實現;這就是操作數前推的具體表現,其中增加的線路稱為旁路;

通過操作數前推,可以有效減少 NOP 的數量,進一步提高流水線效率;

控制冒險#

高級程序中的 for 循環和 if 語句,到了彙編代碼,都會生成 cmp 比較指令和 jmp 等跳轉指令,下一條指令究竟是 pc 寄存器中的值,還是 jmp 對應的地址,CPU 必須等待執行完 cmp 指令後才知道;當決策依賴於一條指令的結果,而其他指令正在執行,這就是控制冒險;

為了防止流水線停頓,我們需要在 cmp 的結果出來之前就對其進行一個預測,這就是分支預測技術;

分支預測技術也分很多種,最簡單的叫做 ** 靜態分支預測;**CPU 認定跳轉不會發生,一直按照順序執行下去,假如預測錯誤,就要丟棄後面執行的指令,這個丟棄的操作是有一定的性能開銷的;

高級一點的叫做動態預測;常見的一種動態預測方案叫做一級分支預測,也叫 1 比特飽和計數,實質為:用一個比特去記錄當前分支的比較情況,用這個值,取預測下一次分支的情況,比如,這一次跳轉了,那麼下一次就去跳轉,否則下一次繼續順序執行;還有一種叫做 2 比特飽和計數,引入了狀態機,相當於下一次分支的預測取決於前兩次的結果;關於分支預測更詳細的內容,可以參考wiki

亂序執行#

上文數據冒險中的操作數前推可以減少很多不必要的 NOP 操作,但是有些情況下,NOP 是不可避免的,為了進一步提高吞吐率,CPU 完全可以在 NOP 這個階段去運行後面沒有依賴的,可以直接運行的指令,這種技術叫做亂序執行;

以下為過程圖:

cpu 乱序执行.png

在保留站,一個指令等待其依賴的數據也傳過來之後,就可以發給功能單元 FU 來執行,這個階段就是亂序執行;

所謂的 FU,其實就是 ALU,但不同 ALU 可以有不同功能,所以亂序執行階段需要盡可能最大化利用這一點;

執行完後,還要放入重排序緩衝區,排成原先指令的順序;

其次,指令計算的結果先提交到存儲緩衝區,最後再到 CPU 的高速緩存或是內存;

在訪問內存和寫回的階段,必須是順序的,這是為了防止多核造成的數據不一致問題:每個 CPU 核都有自己的緩存和寄存器,假如寫內存的順序不一致,就會出現預期之外的錯誤;

在這一過程中,從外部看依舊是順序的,而內部各個 FU 都沒有閒置,CPU 吞吐率進一步提提升;

超標量技術#

即使上文各種 CPU 性能提高技術都用上,IPC(一個時鐘周期能夠執行的指令數)也不會超過 1,因為 CPU 在一個時鐘周期只能取得一條指令;

即然指令的執行階段可以通過亂序執行技術來並行,那取指令和指令譯碼也可以考慮採用並行,即:一次性從內存取出多條指令,發給多個指令譯碼器進行指令譯碼:

這種技術,叫做多發射與超標量:

cpu 超标量.png

現代 CPU 採取這種技術,使得 IPC 往往可以達到 2 以上。

總結#

本文的目的為介紹 CPU 性能提高的常見技術;

文章從流水線的介紹講起,到流水線的三大冒險,涉及拆分高速緩存,流水線冒泡,操作數前推,分支預測等內容,再到亂序執行和超標量技術,用並行的方式更進一步利用資源,提高 CPU 的吞吐率。

掌握這些內容,即可對現代 CPU 性能提高有一個比較宏觀的了解。

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。