CPU流水線的探秘之旅

jopen 11年前發布 | 13K 次閱讀 CPU

        作為程序員,CPU 在我們的工作中扮演了核心角色,因此了解處理器內部的工作方式對程序員來說不無裨益。

        CPU 是如何工作的呢?一條指令執行需要多長時間?當我們討論某個新款處理器擁有 12 級流水線還是 18 級流水線,甚至是更深的 31 級流水線時,這到些都意味著什么呢?

        應用程序通常會將 CPU 看作是黑盒子。程序中的指令按照順序依次進入 CPU,執行完之后再按順序依次從 CPU 中出來,而內部到底發生了什么,我們通常并不了解。

        對我們程序員來說,尤其是對做程序性能調優工作的程序員來說,學習 CPU 內部的細節非常必要。否則,如果你不知道 CPU 的內部結構,那如何才能針對 CPU 做性能優化?

        本文所關注的就是專門針對 X86 處理器流水線的工作原理。

        你需要掌握的預備知識

        首先,閱讀本文你需要了解編程,最好了解一點匯編語言。如果你還不知道指令指針(instruction pointer)是什么,那么本文對你來說可能有些難。你需要知道什么是寄存器,指令和緩存,如果不明白它們是什么,你需要盡快查找資料了解一下。

        第二,CPU 的工作原理是一個非常龐大和復雜的話題,本文僅僅是匆匆一瞥,很難以用一篇文章詳盡敘述。如果我有什么疏漏,請通過評論告訴我。

        第三,我僅僅關注英特爾處理器及其 X86 架構。當然除了 X86,還有很多其他架構的處理器。雖然 AMD 公司引入了很多新特性到 X86 架構,但是 X86 架構是 Intel 公司發明,并且創造了 X86 指令集,其中絕大多數特性是由 Intel 引入的。所以為了保持敘述的簡單和一致性,我僅關注 Intel 的處理器。

        最后,當你讀到這篇文章時,它已經是“過時”的了。更新款的處理器已經設計出來,其中一些會在未來幾個月之內發布。我很高興技術能如此快速的發展,我希望有一天所有這些技術都會過時,創造出擁有更驚人計算能力的 CPU。

        處理器流水線基礎

        從一個非常廣的角度來說,X86 處理器架構在近 35 年來并沒有變化太多。雖然 X86 架構被附加了很多新功能,但是最初的設計(包括幾乎所有最初的指令集)仍然基本上是完整保留的,即使在最新的處理器上仍然被支持。

        最初的 8086 處理器支持 14 個寄存器,這些寄存器在如今最新的處理器中仍然存在。這 14 個寄存器中,有 4 個是通用寄存器:AX,BX,CX 和 DX;有 4 個是段寄存器,段寄存器用來輔助指針的實現:代碼段(CS),數據段(DS),擴展段(ES)和堆棧段(SS);有 4 個是索引寄存器,用來指向內存地址:源引用(SI),目的引用(DI),基指針(BP),棧指針(SP);有 1 個寄存器包含狀態位;最后是最重要的寄存器:指令指針(IP)。

        指令指針寄存器是一個擁有特殊功能的指針。指令指針的功能是指向將要運行的下一條指令。

        所有的 X86 處理器都按照相同的模式運行。首先,根據指令指針指向的地址取得下一條即將運行的指令并解析該指令(譯碼)。在譯碼完成后,會有一個指令的執行階段。有些 指令用來從內存讀取數據或者向內存寫數據,有些指令用來執行計算或者比較等工作。當指令執行完成后,這條指令會通過退出(retire)階段并將指令指針 修改為下一條指令。

        譯碼,執行和退出三級流水線組成了 X86 處理器指令執行的基本模式。從最初的 8086 處理器到最新的酷睿 i7 處理器都基本遵循了這樣的過程。雖然更新的處理器增加了更多的流水級,但基本的模式沒有改變。

        35 年來發生了什么改變

        相較于現今的標準,最初的處理器設計顯得太過簡單。最初的 8086 處理器的執行過程可以簡述為從當前指令指針取得指令,通過譯碼,執行最后退出,然后繼續從指令指針指向的下一條指令處取得指令。

        新的處理器增加了新的功能,有些增加了新的指令,有些增加了新的寄存器。我將主要關注和本文主題有關系的改變,這些改變影響了 CPU 指令執行的流程。其他的一些變化比如虛擬內存或者并行處理雖然都很有意義而且有趣,但是并不在本文主題的范圍內。

        指令緩存在 1982 年被加入到處理器中。通過指令緩存,處理器可以一次性從內存讀取更多指令并放在指令緩存中,而不用每條指令都從內存中取。指令緩存僅有幾個字節大小,只能容納數條指令,但是因為消除了之后每次取指往返內存和處理器的時間,極大的提高的效率

        1985 年的 386 處理器引入了數據緩存,而且擴展了指令緩存的設計。數據訪存請求通過一次性讀取更多的數據放在數據緩存中,從而提升了性能。而且,數據緩存和指令緩存都從幾個字節擴大到幾千字節。

        1989 年推出的 i486 處理器引入了五級流水線。這時,在 CPU 中不再僅運行一條指令,每一級流水線在同一時刻都運行著不同的指令。這個設計使得 i486 比同頻率的 386 處理器性能提升了不止一倍。五級流水線中的取指階段將指令從指令緩存中取出(i486 中的指令緩存為 8KB);第二級為譯碼階段,將取出的指令翻譯為具體的功能操作;第三級為轉址階段,用來將內存地址和偏移進行轉換;第四級為執行階段,指令在該階段真正 執行運算;第五級為退出階段,運算的結果被寫回寄存器或者內存。由于處理器同時運行了多條指令,大大提升了程序運行的性能。

        1993 年 Intel 推出了奔騰(Pentium)處理器。由于訴訟問題,Intel 無法繼續沿用原來的數字編號。因此,用奔騰替代了 586 作為新款處理器的代號。奔騰處理器相對 i486 處理器對流水線做出了更多修改。奔騰處理器架構增加了第二條獨立的超標量流水線。主流水線工作方式類似于 i486,第二條流水線則并行的運行一些較簡單的指令,比如說定點算術,而且該流水線能更快的進行該運算。

        1995 年 Intel 推出了奔騰 Pro (Pentium Pro)處理器。和之前的處理器相比,奔騰 Pro 采用了完全不同的設計。該處理器采用了諸多新特性以提高性能,包括亂序(Out-of-Order, OOO)執行的部件以及猜測執行。流水線擴展到了 12 級,而且引入了“超標量流水線”的概念,使得許多指令可以被同時處理。我們稍后將詳盡的介紹亂序執行的部件。

        在 1995-2002 年之間,亂序執行部件經過了數次重大改進。處理器中加入了更多的寄存器;單指令多數據(Single Instruction Multiple Data, or SIMD)的引入使得一條指令可以進行多組數據運算;現有的緩存變得更大而且引入了新的緩存;有些流水級被拆分成更多流水級,有些流水級被合并,使得更加 適合實際的應用。這些改變對整體性能的提升有重要作用,但它們都沒有從根本影響數據在處理器中的流動方式。

        2002 年發布的奔騰 4 處理器引入了超線程技術。亂序執行部件的設計使得指令被執行的速度比處理器能夠提供指令的速度更快。因此對于大部分應用,CPU 的亂序執行部件在大部分時間處于空閑狀態,甚至在高負載的情況下也不能充分利用。為了讓指令流能充分的流入亂序執行部件,Intel 加入了第二套前端部件(譯注:在處理器結構中,前端是指取指,譯碼,寄存器重命名等模塊,經過前端部件的處理后,指令等待發射進入亂序執行部件)。雖然實 際上只有一個亂序執行部件,但對于操作系統來說,它能看到兩個處理器。前端部件包含兩組同樣功能的 X86 寄存器,兩個指令譯碼器根據兩個指令指針指向的地址分別處理。所有的指令被一個共享的亂序執行部件執行,但對應用程序來說并不知情。當亂序執行部件執行完 成,像之前一樣退出流水線后,最終結果返回虛擬的兩個處理器。

        2006 年 Intel 發布了酷睿(Core)微架構。為了品牌效應,它被稱做酷睿2(二總比一好)。令人驚訝的是,處理器頻率不升反降,而且超線程也被去掉了。通過降低時鐘頻 率,每一級流水線可以做更多工作。亂序執行部件也被擴展的更寬。各種不同的緩存和隊列都相應做的更大。而且處理器被重新設計,以適應雙核和四核的共享緩存 結構。

        2008 年,Intel 開始用酷睿 i3, i5, i7 的方式來命名新的處理器。新處理器重新引入了超線程。這三個系列的處理器主要區別在于內部緩存大小不同。

        未來的處理器:Intel 的下一代微結構被稱為 Haswell。Haswell 據稱將于 2013 年發布。目前已知的文檔說明它將擁有 14 級流水級的亂序執行部件,所以它仍然遵循從奔騰 Pro 以來的基本設計思路。

        那么,流水線到底是什么?亂序執行部件是什么?他們如何提升了處理器的性能呢?

        CPU 指令流水線

        根據之前描述的基礎,指令進入流水線,通過流水線處理,從流水線出來的過程,對于我們程序員來說,是比較直觀的。

        I486 擁有五級流水線。分別是:取指(Fetch),譯碼(D1, main decode),轉址(D2, translate),執行(EX, execute),寫回(WB)。某個指令可以在流水線的任何一級。

CPU流水線的探秘之旅

        但是這樣的流水線有一個明顯的缺陷。對于下面的指令代碼,它們的功能是將兩個變量的內容進行交換。

        1

        2

        3

        XOR a, b

        XOR b, a

        XOR a, b

        從 8086 直到 386 處理器都沒有流水線。處理器一次只能執行一條指令。再這樣的架構下,上面的代碼執行并不會存在問題。

        但是 i486 處理器是首個擁有流水線的 x86 處理器,它執行上面的代碼會發生什么呢?當你一下去觀察很多指令在流水線中運行,你會覺得混亂,所以你需要回頭參考上面的圖。

        第一步是第一條指令進入取指階段;然后在第二步第一條指令進入譯碼階段,同時第二條指令進入取指階段;第三步第一條指令進入轉址階段,第二條指 令進入譯碼階段,第三條指令進入取指階段。但是在第四步會出現問題,第一條指令會進入執行階段,而其他指令卻不能繼續向前移動。第二條 xor 指令需要第一條 xor 指令計算的結果a,但是直到第一條指令執行完成才會寫回。所以流水線的其他指令就會在當前流水級等待直到第一條指令的執行和寫回階段完成。第二條指令會等 待第一條指令完成才能進入流水線下一級,同樣第三條指令也要等待第二條指令完成。

        這個現象被稱為流水線阻塞或者流水線氣泡。

        另外一個關于流水線的問題是有些指令執行速度快,有些指令執行速度慢。這個問題在奔騰處理器的雙流水線架構下顯得更加明顯。

        奔騰 Pro 擁有 12 級流水線。當這個數字被首次宣布后,所有的程序員都倒抽了一口氣,因為他們知道超標量流水線是如何工作的。如果 Intel 仍然按照以前的思路設計超標量流水線的話,流水線的阻塞和執行速度慢的指令會嚴重影響執行速度。但同時,Intel 宣布了完全不同的流水線設計,叫做亂序執行部件(Out-of-Order core)。單從敘述上很難理解這些改變帶來的好處,但 Intel 確信這些改進是令人激動的。

        讓我們來更深入的看看這個亂序執行的部件吧!

        亂序執行流水線

        在描述亂序執行流水線時,往往是一圖勝千言。所以我們主要以圖例進行介紹。

        CPU 流水線圖例

        I486 處理器擁有 5 級流水線。這種設計在現實世界中的其他處理器中很常見,而且效率不錯。

CPU流水線的探秘之旅

        而奔騰處理器的流水線比 i486 更好。兩條流水線可以并行運行,而且每條流水線可以同時有多條指令在不同流水級執行。它幾乎可以同時執行比 i486 多一倍的指令。

CPU流水線的探秘之旅

        能夠快速完成的指令需要等待前面執行慢的指令即使在并行流水線中也仍然是一個問題。流水線仍然是線性的,導致處理器面臨性能瓶頸難以逾越。

        亂序執行部件和之前處理器設計中的線性通路有很大不同,它增加了一些復雜度,引入了非線性的通路。

CPU流水線的探秘之旅

        第一個改變是指令從內存中取到處理器的指令緩存的過程。現代處理器能夠檢測何時會產生一個大的分支跳轉(比如函數調用),然后提前將跳轉目的地的指令加載到指令緩存中。

        譯碼級有一些略微的修改。不同于以往處理器僅僅譯碼指令指針指向的指令,奔騰 Pro 處理器每一個始終周期最多能譯碼 3 條指令。現今的處理器(2008-2013 年)每個時鐘周期最多可以譯碼 4 條指令。譯碼過程產生很多小片的操作,被稱作微指令(micro-ops, μ-ops)。

        下一級(或者好幾級)被稱為微指令翻譯,接著是寄存器重命名(register aliasing)。許多操作同時執行,并且執行的順序是亂序的,所以有可能出現一條指令讀一個寄存器的同時,另外一條指令正在對這個寄存器進行寫操作。 在處理器內部,這些原始的寄存器(如 AX,BX,CX,DX 等)被翻譯(或者重命名)成為內部的寄存器,而這些寄存器對程序員是不可見的。寄存器和內存地址需要被映射到一個臨時的地方用于指令執行。當前每個始終周 期可以翻譯 4 條微指令。

        當微指令翻譯完成后,它們會進入一個重排序緩存(Reorder Buffer, ROB),ROB 可以存儲最多 128 條微指令。在支持超線程的處理器上,ROB 同樣可以重排來自兩個虛擬處理器的指令。兩個虛擬處理器在 ROB 中將微指令匯集到一個共享的亂序執行部件中。

        這些微指令已經準備好可以執行了。它們被放在保留站中(Reservation Station, RS)。RS 最多可以同時存儲 36 條微指令。

        現在才開始亂序執行部件神奇的部分。不同的微指令在不同的執行單元中同時執行,而且每個執行單元都全速運行。只要當前微指令所需要的數據就緒, 而且有空閑的執行單元,微指令就可以立即執行,有時甚至可以跳過前面還未就緒的微指令。通過這種方式,需要長時間運行的操作不會阻塞后面的操作,流水線阻 塞帶來的損失被極大的減小了。

        奔騰 Pro 的亂序執行部件擁有 6 個執行單元:兩個定點處理單元,一個浮點處理單元,一個取數單元,一個存地址單元,一個存數單元。這兩個定點處理單元有所不同,一個能夠處理復雜定點操 作,一個能同時處理兩個簡單操作。在理想狀況下,奔騰 Pro 的亂序執行部件可以在一個時鐘周期內執行 7 條微指令。

        現今的亂序執行部件仍然擁有 6 個執行單元。其中取數單元,存地址單元,存數單元沒有變,另外 3 個多少發生了變化。這三個執行單元都可以執行基本算術運算,或者執行更復雜的微指令。但每個執行單元擅長執行不同種類的微指令,使得它們能更高效的執行運 算。在理想狀況下,現今的亂序執行部件可以在一個時鐘周期內執行 11 條微指令。

        最終微指令會得到執行,在經過數個流水級之后,最終會退出流水線。這時,這條指令完成并且遞增指令指針。但從程序員的角度來說,指令僅僅是從一端進入 CPU,從另一端退出,就像老的 8086 一樣。

        如果你仔細看過上面的內容,你會注意到上面提到過很重要的一個問題:如果執行指令的位置發生了跳轉會發生什么?例如,當指令運行到”if”或者是”switch”時,會發生什么呢?在較老的處理器中這意味著清空流水線,等待新的跳轉目的指令的取指執行。

        當 CPU 指令隊列中存儲了超過 100 條指令時,發生流水線阻塞帶來的性能損失是極其嚴重的。所有的指令都需要等待跳轉目的的指令取回并且重啟流水線。在這種情況下,亂序執行部件需要將跳轉指 令之后但是已經執行的微指令全部取消掉,返回到執行前的狀態。當所有亂序執行的微指令都退出亂序執行部件之后,將它們丟棄掉,然后從新的地址開始執行。這 對于處理器來說是相當困難的,而且發生的頻率很高,因此對性能的影響很大。這時,引入了亂序執行部件的另外一個重要功能。

        答案就是猜測執行。猜測執行意味著當遇到一個分支指令后,亂序執行部件會將所有分支的指令都執行一遍。一旦分支指令的跳轉方向確定后,錯誤跳轉 方向的指令都將被丟棄。通過同時執行兩個跳轉方向的指令,避免了由于分支跳轉導致的阻塞。處理器設計者還發明了分支預測緩存,當面臨多個分支時進行預測, 進一步提高了性能。雖然 CPU 阻塞仍然會發生,但是這個解決方案將 CPU 發生阻塞的概率降到了一個可以接受的范圍。

        最后,擁有超線程的處理器將兩個虛擬的處理器暴露給共享的亂序執行部件。它們共享一個重排序緩存和亂序執行部件,讓操作系統認為它們是兩個獨立的處理器,看上去就像這樣:

CPU流水線的探秘之旅

        超線程的處理器擁有兩個虛擬的處理器,從而可以給亂序執行部件提供更多的數據。超線程對一般的應用程序都有性能提升,但是對一些計算密集型的應 用,則會迅速使得亂序執行部件飽和。在這種情況下,超線程反而會略微降低性能。但這種情況畢竟是少數,超線程對于日常應用來講通常都能夠提供大約一倍的性 能。

        一個示例

        這一切看上去有點令人感到困惑,那么我們舉一個例子來讓這一切變得清晰起來。

        從應用程序的角度來看,我們仍然是運行在指令流水線上,就想老的 8086 處理器那樣。處理器就是一個黑盒子。黑盒子會處理指令指針指向的指令,當處理完之后,會在內存里找到處理的結果。

        但是從指令本身的角度來講,這個過程可謂歷經滄桑。我們下面介紹對于現今的處理器(大約在 2008-2013 年之間),一條指令在其內部的過程。

        首先,你是一條指令,你所屬的程序正在運行。

        你一直在耐心的等待指令指針會指向自己,等待被 CPU 運行。當指令指針距離你還有 4KB 遠的時候(這大約是 1500 條指令),你被 CPU 從內存取到指令緩存中。雖然從內存加載進入指令緩存需要一段時間,但是現在距離你被執行的時刻還很遠,你有足夠的時間。這個預取的過程屬于流水線的第一 級。

        當指令指針離你越來越近,距離你還有 24 條指令的時候,你和你旁邊的 5 個指令會被放到指令隊列里面。

        這個處理器有 4 個譯碼器,可以容納一個復雜指令和最多三個簡單指令。你碰巧是一條復雜指令,通過譯碼,你被翻譯成 4 個微指令。

        譯碼的過程可以劃分為多步。譯碼過程中的一步是檢查你需要的數據和猜測你可能會產生一個地址跳轉。譯碼器一旦檢測到需要的額外數據,不需要讓你知道,這個數據就開始從內存加載到數據緩存中了。

        你的四個微指令到達寄存器重命名表。你告訴它你需要讀哪個內存地址(比如說 fs:[eax+18h]),然后寄存器重命名表將這個地址轉換為臨時地址供微指令使用。地址轉化完成后,你的微指令將進入重排序緩存(Reorder Buffer, ROB)并記錄指令次序。接著第一時間進入保留站(Reservation Station, RS)。

        保留站用于存儲已經準備就緒可以執行的指令。你的第三條微指令被立即選中并送往端口5,這個端口直接執行運算。但是你并不知道為什么它會被首先 選中,無論如何,它確實被執行了。幾個時鐘周期之后你的第一條微指令前往端口2,該端口是讀單元(Load Address execution unit)。剩余的微指令一直等待,同時各個端口正在收集不同的微指令。他們都在等待端口 2 將數據從緩存和內存中加載進來并放在臨時存儲空間內。

        他們等了很久……

        相當久的時間……

        不過在他們等待第一條微指令返回數據的時候,又有其他的新指令又進來。好在處理器知道如何讓這些指令亂序執行(即后到達保留站的微指令被優先執行)。

        當第一條微指令返回了數據,剩余的兩條微指令被立即送往執行端口 0 和1。現在這 4 條微指令都已經運行,最終它們會返回保留站。

        這些微指令返回后交出他們的“票”并給出各自的臨時地址。通過這些地址,你作為一個完整的指令,將他們合并。最后 CPU 將結果交給你并使你退出

        當你到達標有“退出”的門的時候,你會發現這里要排一個隊列。你進入后發現你剛好站在你前面進來指令的后面,即使執行中的順序可能已經不同,但你們退出的順序繼續保持一致。看來亂序執行部件真正知道自己做了什么。

        每條指令最終離開 CPU,每次一條指令,就和指令指針指向的順序一樣!

        結論

        希望這篇小文能夠給讀者展示一些處理器工作的奧秘,要知道,這并不是魔術。

        讓我們回到最初的問題,現在我們應該可以給出一些較好的答案了。

        處理器內部是如何工作的呢?在這個復雜的過程中,指令首先被分解為更小的微指令命令,這些微指令以亂序的方式盡可能快的被執行,然后按照原始的 順序提交執行結果。因此,從外部看來,所有的指令都是按照順序的方式執行的。但是現在我們知道,處理器內部是以亂序的方式處理指令的,有時甚至以猜測的方 式來運行分支代碼。

        運行一條指令究竟需要多長時間呢?對于沒有使用流水線技術的處理器來說,這是一個容易回答的問題,但對于現代的處理器來說,一條指令的執行時間 與它周圍指令的內容以及臨近 cache 的大小和內容都有關。一條指令通過處理器有一個最小的時間,但只能粗略的說這個時間是恒定的。一個好的程序員和編譯器可以讓很多條指令同時運行,從而使每 條指令的分攤時間幾乎為零。這里說的幾乎為零的執行時間并不是指一條指令的總的執行時間很短,相反,通過整個亂序部件和等待內存讀寫數據是需要花費很多時 間的。

        一個新的處理器擁有 12 級或者 18 級、甚至更深的 31 級流水線意味著什么呢?這意味著更多的指令可以被同時送進加工廠。一個非常深的流水線可以讓幾百條指令同時被處理。當一切順利時,一個亂序部件可以保持高 速運轉,從而獲得驚人的吞吐量。不幸的是,深的流水線同時意味著流水線停頓會從一個相對可以容忍的性能損失變成一個可怕的性能噩夢。因為幾百條指令都不得 不停頓下來,等待流水線恢復運轉。

        我怎么根據這些信息來優化程序呢?幸運的是,CPU 可以在大部分常見情況下工作良好,并且編譯器已經為亂序處理器優化了近 20 年。當指令和數據按照順序(沒有煩人的跳轉)執行時,CPU 可以獲得最好的性能。因此,首先,使用簡單的代碼。簡單直接的代碼會幫助編譯器的優化引擎識別并優化代碼。盡量不使用跳轉指令,當你不得不跳轉時,盡量每 次跳轉到同樣的方向。復雜的設計,例如動態跳轉表,雖然看起來很酷并且的確可以完成非常強大的功能,但不管是處理器還是編譯器,都無法進行很好的預測處 理,因此復雜的代碼很可能導致流水線停頓和猜測錯誤,從而極大的損害處理器性能。其次,使用簡單的數據結構。保持數據順序、相鄰和連續可以阻止數據停頓。 使用正確的數據結構和數據分布可以獲得很大的性能提升。只要保持代碼和數據結構盡量簡單,剩下的工作就可以放心地交給編譯器的優化引擎來完成了。

        感謝與我一起參與這次旅行!

        編譯:感謝@deuso_ICT 的熱心翻譯

英文原文:A Journey Through the CPU Pipeline

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!