探秘X86架構(gòu)CPU流水線
英文原文:A Journey Through the CPU Pipeline
作為程序員,CPU 在我們的工作中扮演了核心角色,因此了解處理器內(nèi)部的工作方式對程序員來說不無裨益。
CPU 是如何工作的呢?一條指令執(zhí)行需要多長時間?當我們討論某個新款處理器擁有 12 級流水線還是 18 級流水線,甚至是更深的 31 級流水線時,這到些都意味著什么呢?
應(yīng)用程序通常會將 CPU 看作是黑盒子。程序中的指令按照順序依次進入 CPU,執(zhí)行完之后再按順序依次從 CPU 中出來,而內(nèi)部到底發(fā)生了什么,我們通常并不了解。
對我們程序員來說,尤其是對做程序性能調(diào)優(yōu)工作的程序員來說,學習 CPU 內(nèi)部的細節(jié)非常必要。否則,如果你不知道 CPU 的內(nèi)部結(jié)構(gòu),那如何才能針對 CPU 做性能優(yōu)化?
本文所關(guān)注的就是專門針對 X86 處理器流水線的工作原理。
你需要掌握的預(yù)備知識
首先,閱讀本文你需要了解編程,最好了解一點匯編語言。如果你還不知道指令指針(instruction pointer)是什么,那么本文對你來說可能有些難。你需要知道什么是寄存器,指令和緩存,如果不明白它們是什么,你需要盡快查找資料了解一下。
第二,CPU 的工作原理是一個非常龐大和復(fù)雜的話題,本文僅僅是匆匆一瞥,很難以用一篇文章詳盡敘述。如果我有什么疏漏,請通過評論告訴我。
第三,我僅僅關(guān)注英特爾處理器及其 X86 架構(gòu)。當然除了 X86,還有很多其他架構(gòu)的處理器。雖然 AMD 公司引入了很多新特性到 X86 架構(gòu),但是 X86 架構(gòu)是 Intel 公司發(fā)明,并且創(chuàng)造了 X86 指令集,其中絕大多數(shù)特性是由 Intel 引入的。所以為了保持敘述的簡單和一致性,我僅關(guān)注 Intel 的處理器。
最后,當你讀到這篇文章時,它已經(jīng)是“過時”的了。更新款的處理器已經(jīng)設(shè)計出來,其中一些會在未來幾個月之內(nèi)發(fā)布。我很高興技術(shù)能如此快速的發(fā)展,我希望有一天所有這些技術(shù)都會過時,創(chuàng)造出擁有更驚人計算能力的 CPU.
處理器流水線基礎(chǔ)
從一個非常廣的角度來說,X86 處理器架構(gòu)在近 35 年來并沒有變化太多。雖然 X86 架構(gòu)被附加了很多新功能,但是最初的設(shè)計(包括幾乎所有最初的指令集)仍然基本上是完整保留的,即使在最新的處理器上仍然被支持。
最初的 8086 處理器支持 14 個寄存器,這些寄存器在如今最新的處理器中仍然存在。這 14 個寄存器中,有 4 個是通用寄存器:AX,BX,CX 和 DX;有 4 個是段寄存器,段寄存器用來輔助指針的實現(xiàn):代碼段(CS),數(shù)據(jù)段(DS),擴展段(ES)和堆棧段(SS);有 4 個是索引寄存器,用來指向內(nèi)存地址:源引用(SI),目的引用(DI),基指針(BP),棧指針(SP);有 1 個寄存器包含狀態(tài)位;最后是最重要的寄存器:指令指針(IP)。
指令指針寄存器是一個擁有特殊功能的指針。指令指針的功能是指向?qū)⒁\行的下一條指令。
所有的 X86 處理器都按照相同的模式運行。首先,根據(jù)指令指針指向的地址取得下一條即將運行的指令并解析該指令(譯碼)。在譯碼完成后,會有一個指令的執(zhí)行階段。有些指令用來從內(nèi)存讀取數(shù)據(jù)或者向內(nèi)存寫數(shù)據(jù),有些指令用來執(zhí)行計算或者比較等工作。當指令執(zhí)行完成后,這條指令會通過退出(retire)階段并將指令指針修改為下一條指令。
譯碼,執(zhí)行和退出三級流水線組成了 X86 處理器指令執(zhí)行的基本模式。從最初的 8086 處理器到最新的酷睿 i7 處理器都基本遵循了這樣的過程。雖然更新的處理器增加了更多的流水級,但基本的模式?jīng)]有改變。
35 年來發(fā)生了什么改變
相較于現(xiàn)今的標準,最初的處理器設(shè)計顯得太過簡單。最初的 8086 處理器的執(zhí)行過程可以簡述為從當前指令指針取得指令,通過譯碼,執(zhí)行最后退出,然后繼續(xù)從指令指針指向的下一條指令處取得指令。
新的處理器增加了新的功能,有些增加了新的指令,有些增加了新的寄存器。我將主要關(guān)注和本文主題有關(guān)系的改變,這些改變影響了 CPU 指令執(zhí)行的流程。其他的一些變化比如虛擬內(nèi)存或者并行處理雖然都很有意義而且有趣,但是并不在本文主題的范圍內(nèi)。
指令緩存在 1982 年被加入到處理器中。通過指令緩存,處理器可以一次性從內(nèi)存讀取更多指令并放在指令緩存中,而不用每條指令都從內(nèi)存中取。指令緩存僅有幾個字節(jié)大小,只能容納數(shù)條指令,但是因為消除了之后每次取指往返內(nèi)存和處理器的時間,極大的提高的效率
1985 年的 386 處理器引入了數(shù)據(jù)緩存,而且擴展了指令緩存的設(shè)計。數(shù)據(jù)訪存請求通過一次性讀取更多的數(shù)據(jù)放在數(shù)據(jù)緩存中,從而提升了性能。而且,數(shù)據(jù)緩存和指令緩存都從幾個字節(jié)擴大到幾千字節(jié)。
19巴久年推出的 i486 處理器引入了五級流水線。這時,在 CPU 中不再僅運行一條指令,每一級流水線在同一時刻都運行著不同的指令。這個設(shè)計使得 I486 比同頻率的 386 處理器性能提升了不止一倍。五級流水線中的取指階段將指令從指令緩存中取出(i486 中的指令緩存為 8KB);第二級為譯碼階段,將取出的指令翻譯為具體的功能操作;第三級為轉(zhuǎn)址階段,用來將內(nèi)存地址和偏移進行轉(zhuǎn)換;第四級為執(zhí)行階段,指令在該階段真正執(zhí)行運算;第五級為退出階段,運算的結(jié)果被寫回寄存器或者內(nèi)存。由于處理器同時運行了多條指令,大大提升了程序運行的性能。
1993 年 Intel 推出了奔騰(Pentium)處理器。由于訴訟問題,Intel 無法繼續(xù)沿用原來的數(shù)字編號。因此,用奔騰替代了 586 作為新款處理器的代號。奔騰處理器相對 i486 處理器對流水線做出了更多修改。奔騰處理器架構(gòu)增加了第二條獨立的超標量流水線。主流水線工作方式類似于 i486,第二條流水線則并行的運行一些較簡單的指令,比如說定點算術(shù),而且該流水線能更快的進行該運算。
1995 年 Intel 推出了奔騰 Pro (Pentium Pro)處理器。和之前的處理器相比,奔騰 Pro 采用了完全不同的設(shè)計。該處理器采用了諸多新特性以提高性能,包括亂序(Out-of-Order, OOO)執(zhí)行的部件以及猜測執(zhí)行。流水線擴展到了 12 級,而且引入了“超標量流水線”的概念,使得許多指令可以被同時處理。我們稍后將詳盡的介紹亂序執(zhí)行的部件。
在 1995-2002 年之間,亂序執(zhí)行部件經(jīng)過了數(shù)次重大改進。處理器中加入了更多的寄存器;單指令多數(shù)據(jù)(Single Instruction Multiple Data, or SIMD)的引入使得一條指令可以進行多組數(shù)據(jù)運算;現(xiàn)有的緩存變得更大而且引入了新的緩存;有些流水級被拆分成更多流水級,有些流水級被合并,使得更加適合實際的應(yīng)用。這些改變對整體性能的提升有重要作用,但它們都沒有從根本影響數(shù)據(jù)在處理器中的流動方式。
2002 年發(fā)布的奔騰 4 處理器引入了超線程技術(shù)。亂序執(zhí)行部件的設(shè)計使得指令被執(zhí)行的速度比處理器能夠提供指令的速度更快。因此對于大部分應(yīng)用,CPU 的亂序執(zhí)行部件在大部分時間處于空閑狀態(tài),甚至在高負載的情況下也不能充分利用。為了讓指令流能充分的流入亂序執(zhí)行部件,Intel 加入了第二套前端部件(譯注:在處理器結(jié)構(gòu)中,前端是指取指,譯碼,寄存器重命名等模塊,經(jīng)過前端部件的處理后,指令等待發(fā)射進入亂序執(zhí)行部件)。雖然實際上只有一個亂序執(zhí)行部件,但對于操作系統(tǒng)來說,它能看到兩個處理器。前端部件包含兩組同樣功能的 X86 寄存器,兩個指令譯碼器根據(jù)兩個指令指針指向的地址分別處理。所有的指令被一個共享的亂序執(zhí)行部件執(zhí)行,但對應(yīng)用程序來說并不知情。當亂序執(zhí)行部件執(zhí)行完成,像之前一樣退出流水線后,最終結(jié)果返回虛擬的兩個處理器。
2006 年 Intel 發(fā)布了酷睿(Core)微架構(gòu)。為了品牌效應(yīng),它被稱做酷睿2(二總比一好)。令人驚訝的是,處理器頻率不升反降,而且超線程也被去掉了。通過降低時鐘頻率,每一級流水線可以做更多工作。亂序執(zhí)行部件也被擴展的更寬。各種不同的緩存和隊列都相應(yīng)做的更大。而且處理器被重新設(shè)計,以適應(yīng)雙核和四核的共享緩存結(jié)構(gòu)。
2008 年,Intel 開始用酷睿 i3, i5, i7 的方式來命名新的處理器。新處理器重新引入了超線程。這三個系列的處理器主要區(qū)別在于內(nèi)部緩存大小不同。
未來的處理器:Intel 的下一代微結(jié)構(gòu)被稱為 Haswell.Haswell 據(jù)稱將于 2013 年發(fā)布。目前已知的文檔說明它將擁有 14 級流水級的亂序執(zhí)行部件,所以它仍然遵循從奔騰 Pro 以來的基本設(shè)計思路。
那么,流水線到底是什么?亂序執(zhí)行部件是什么?他們?nèi)绾翁嵘颂幚砥鞯男阅苣?
CPU 指令流水線
根據(jù)之前描述的基礎(chǔ),指令進入流水線,通過流水線處理,從流水線出來的過程,對于我們程序員來說,是比較直觀的。
I486 擁有五級流水線。分別是:取指(Fetch),譯碼(D1, main decode),轉(zhuǎn)址(D2, translate),執(zhí)行(EX, execute),寫回(WB)。某個指令可以在流水線的任何一級。
但是這樣的流水線有一個明顯的缺陷。對于下面的指令代碼,它們的功能是將兩個變量的內(nèi)容進行交換。
XOR a, b
XOR b, a
XOR a, b
從 8086 直到 386 處理器都沒有流水線。處理器一次只能執(zhí)行一條指令。再這樣的架構(gòu)下,上面的代碼執(zhí)行并不會存在問題。
但是 i486 處理器是首個擁有流水線的 x86 處理器,它執(zhí)行上面的代碼會發(fā)生什么呢?當你一下去觀察很多指令在流水線中運行,你會覺得混亂,所以你需要回頭參考上面的圖。
第一步是第一條指令進入取指階段;然后在第二步第一條指令進入譯碼階段,同時第二條指令進入取指階段;第三步第一條指令進入轉(zhuǎn)址階段,第二條指令進入譯碼階段,第三條指令進入取指階段。但是在第四步會出現(xiàn)問題,第一條指令會進入執(zhí)行階段,而其他指令卻不能繼續(xù)向前移動。第二條 xor 指令需要第一條 xor 指令計算的結(jié)果a,但是直到第一條指令執(zhí)行完成才會寫回。所以流水線的其他指令就會在當前流水級等待直到第一條指令的執(zhí)行和寫回階段完成。第二條指令會等待第一條指令完成才能進入流水線下一級,同樣第三條指令也要等待第二條指令完成。
這個現(xiàn)象被稱為流水線阻塞或者流水線氣泡。
另外一個關(guān)于流水線的問題是有些指令執(zhí)行速度快,有些指令執(zhí)行速度慢。這個問題在奔騰處理器的雙流水線架構(gòu)下顯得更加明顯。
奔騰 Pro 擁有 12 級流水線。當這個數(shù)字被首次宣布后,所有的程序員都倒抽了一口氣,因為他們知道超標量流水線是如何工作的。如果 Intel 仍然按照以前的思路設(shè)計超標量流水線的話,流水線的阻塞和執(zhí)行速度慢的指令會嚴重影響執(zhí)行速度。但同時,Intel 宣布了完全不同的流水線設(shè)計,叫做亂序執(zhí)行部件(Out-of-Order core)。單從敘述上很難理解這些改變帶來的好處,但 Intel 確信這些改進是令人激動的。
讓我們來更深入的看看這個亂序執(zhí)行的部件吧!
亂序執(zhí)行流水線
在描述亂序執(zhí)行流水線時,往往是一圖勝千言。所以我們主要以圖例進行介紹。
CPU 流水線圖例
I486 處理器擁有 5 級流水線。這種設(shè)計在現(xiàn)實世界中的其他處理器中很常見,而且效率不錯。
而奔騰處理器的流水線比 i486 更好。兩條流水線可以并行運行,而且每條流水線可以同時有多條指令在不同流水級執(zhí)行。它幾乎可以同時執(zhí)行比 i486 多一倍的指令。
能夠快速完成的指令需要等待前面執(zhí)行慢的指令即使在并行流水線中也仍然是一個問題。流水線仍然是線性的,導致處理器面臨性能瓶頸難以逾越。
亂序執(zhí)行部件和之前處理器設(shè)計中的線性通路有很大不同,它增加了一些復(fù)雜度,引入了非線性的通路。[!--empirenews.page--]
第一個改變是指令從內(nèi)存中取到處理器的指令緩存的過程?,F(xiàn)代處理器能夠檢測何時會產(chǎn)生一個大的分支跳轉(zhuǎn)(比如函數(shù)調(diào)用),然后提前將跳轉(zhuǎn)目的地的指令加載到指令緩存中。
譯碼級有一些略微的修改。不同于以往處理器僅僅譯碼指令指針指向的指令,奔騰 Pro 處理器每一個時鐘周期最多能譯碼 3 條指令?,F(xiàn)今的處理器(2008-2013 年)每個時鐘周期最多可以譯碼 4 條指令。譯碼過程產(chǎn)生很多小片的操作,被稱作微指令(micro-ops, ?-ops)。
下一級(或者好幾級)被稱為微指令翻譯,接著是寄存器重命名(register aliasing)。許多操作同時執(zhí)行,并且執(zhí)行的順序是亂序的,所以有可能出現(xiàn)一條指令讀一個寄存器的同時,另外一條指令正在對這個寄存器進行寫操作。在處理器內(nèi)部,這些原始的寄存器(如 AX,BX,CX,DX 等)被翻譯(或者重命名)成為內(nèi)部的寄存器,而這些寄存器對程序員是不可見的。寄存器和內(nèi)存地址需要被映射到一個臨時的地方用于指令執(zhí)行。當前每個始終周期可以翻譯 4 條微指令。
當微指令翻譯完成后,它們會進入一個重排序緩存(Reorder Buffer, ROB),ROB 可以存儲最多 128 條微指令。在支持超線程的處理器上,ROB 同樣可以重排來自兩個虛擬處理器的指令。兩個虛擬處理器在 ROB 中將微指令匯集到一個共享的亂序執(zhí)行部件中。
這些微指令已經(jīng)準備好可以執(zhí)行了。它們被放在保留站中(Reservation Station, RS)。RS 最多可以同時存儲 36 條微指令。
現(xiàn)在才開始亂序執(zhí)行部件神奇的部分。不同的微指令在不同的執(zhí)行單元中同時執(zhí)行,而且每個執(zhí)行單元都全速運行。只要當前微指令所需要的數(shù)據(jù)就緒,而且有空閑的執(zhí)行單元,微指令就可以立即執(zhí)行,有時甚至可以跳過前面還未就緒的微指令。通過這種方式,需要長時間運行的操作不會阻塞后面的操作,流水線阻塞帶來的損失被極大的減小了。
奔騰 Pro 的亂序執(zhí)行部件擁有 6 個執(zhí)行單元:兩個定點處理單元,一個浮點處理單元,一個取數(shù)單元,一個存地址單元,一個存數(shù)單元。這兩個定點處理單元有所不同,一個能夠處理復(fù)雜定點操作,一個能同時處理兩個簡單操作。在理想狀況下,奔騰 Pro 的亂序執(zhí)行部件可以在一個時鐘周期內(nèi)執(zhí)行 7 條微指令。
現(xiàn)今的亂序執(zhí)行部件仍然擁有 6 個執(zhí)行單元。其中取數(shù)單元,存地址單元,存數(shù)單元沒有變,另外 3 個多少發(fā)生了變化。這三個執(zhí)行單元都可以執(zhí)行基本算術(shù)運算,或者執(zhí)行更復(fù)雜的微指令。但每個執(zhí)行單元擅長執(zhí)行不同種類的微指令,使得它們能更高效的執(zhí)行運算。在理想狀況下,現(xiàn)今的亂序執(zhí)行部件可以在一個時鐘周期內(nèi)執(zhí)行 11 條微指令。
最終微指令會得到執(zhí)行,在經(jīng)過數(shù)個流水級之后,最終會退出流水線。這時,這條指令完成并且遞增指令指針。但從程序員的角度來說,指令僅僅是從一端進入 CPU,從另一端退出,就像老的 8086 一樣。
如果你仔細看過上面的內(nèi)容,你會注意到上面提到過很重要的一個問題:如果執(zhí)行指令的位置發(fā)生了跳轉(zhuǎn)會發(fā)生什么?例如,當指令運行到“if”或者是“switch”時,會發(fā)生什么呢?在較老的處理器中這意味著清空流水線,等待新的跳轉(zhuǎn)目的指令的取指執(zhí)行。
當 CPU 指令隊列中存儲了超過 100 條指令時,發(fā)生流水線阻塞帶來的性能損失是極其嚴重的。所有的指令都需要等待跳轉(zhuǎn)目的的指令取回并且重啟流水線。在這種情況下,亂序執(zhí)行部件需要將跳轉(zhuǎn)指令之后但是已經(jīng)執(zhí)行的微指令全部取消掉,返回到執(zhí)行前的狀態(tài)。當所有亂序執(zhí)行的微指令都退出亂序執(zhí)行部件之后,將它們丟棄掉,然后從新的地址開始執(zhí)行。這對于處理器來說是相當困難的,而且發(fā)生的頻率很高,因此對性能的影響很大。這時,引入了亂序執(zhí)行部件的另外一個重要功能。
答案就是猜測執(zhí)行。猜測執(zhí)行意味著當遇到一個分支指令后,亂序執(zhí)行部件會將所有分支的指令都執(zhí)行一遍。一旦分支指令的跳轉(zhuǎn)方向確定后,錯誤跳轉(zhuǎn)方向的指令都將被丟棄。通過同時執(zhí)行兩個跳轉(zhuǎn)方向的指令,避免了由于分支跳轉(zhuǎn)導致的阻塞。處理器設(shè)計者還發(fā)明了分支預(yù)測緩存,當面臨多個分支時進行預(yù)測,進一步提高了性能。雖然 CPU 阻塞仍然會發(fā)生,但是這個解決方案將 CPU 發(fā)生阻塞的概率降到了一個可以接受的范圍。
最后,擁有超線程的處理器將兩個虛擬的處理器暴露給共享的亂序執(zhí)行部件。它們共享一個重排序緩存和亂序執(zhí)行部件,讓操作系統(tǒng)認為它們是兩個獨立的處理器,看上去就像這樣:
超線程的處理器擁有兩個虛擬的處理器,從而可以給亂序執(zhí)行部件提供更多的數(shù)據(jù)。超線程對一般的應(yīng)用程序都有性能提升,但是對一些計算密集型的應(yīng)用,則會迅速使得亂序執(zhí)行部件飽和。在這種情況下,超線程反而會略微降低性能。但這種情況畢竟是少數(shù),超線程對于日常應(yīng)用來講通常都能夠提供大約一倍的性能。
一個示例
這一切看上去有點令人感到困惑,那么我們舉一個例子來讓這一切變得清晰起來。
從應(yīng)用程序的角度來看,我們?nèi)匀皇沁\行在指令流水線上,就想老的 8086 處理器那樣。處理器就是一個黑盒子。黑盒子會處理指令指針指向的指令,當處理完之后,會在內(nèi)存里找到處理的結(jié)果。
但是從指令本身的角度來講,這個過程可謂歷經(jīng)滄桑。我們下面介紹對于現(xiàn)今的處理器(大約在 2008-2013 年之間),一條指令在其內(nèi)部的過程。
首先,你是一條指令,你所屬的程序正在運行。
你一直在耐心的等待指令指針會指向自己,等待被 CPU 運行。當指令指針距離你還有 4KB 遠的時候(這大約是 1500 條指令),你被 CPU 從內(nèi)存取到指令緩存中。雖然從內(nèi)存加載進入指令緩存需要一段時間,但是現(xiàn)在距離你被執(zhí)行的時刻還很遠,你有足夠的時間。這個預(yù)取的過程屬于流水線的第一級。
當指令指針離你越來越近,距離你還有 24 條指令的時候,你和你旁邊的 5 個指令會被放到指令隊列里面。
這個處理器有 4 個譯碼器,可以容納一個復(fù)雜指令和最多三個簡單指令。你碰巧是一條復(fù)雜指令,通過譯碼,你被翻譯成 4 個微指令。
譯碼的過程可以劃分為多步。譯碼過程中的一步是檢查你需要的數(shù)據(jù)和猜測你可能會產(chǎn)生一個地址跳轉(zhuǎn)。譯碼器一旦檢測到需要的額外數(shù)據(jù),不需要讓你知道,這個數(shù)據(jù)就開始從內(nèi)存加載到數(shù)據(jù)緩存中了。
你的四個微指令到達寄存器重命名表。你告訴它你需要讀哪個內(nèi)存地址(比如說 fs:[eax+18h]),然后寄存器重命名表將這個地址轉(zhuǎn)換為臨時地址供微指令使用。地址轉(zhuǎn)化完成后,你的微指令將進入重排序緩存(Reorder Buffer, ROB)并記錄指令次序。接著第一時間進入保留站(Reservation Station, RS)。
保留站用于存儲已經(jīng)準備就緒可以執(zhí)行的指令。你的第三條微指令被立即選中并送往端口5,這個端口直接執(zhí)行運算。但是你并不知道為什么它會被首先選中,無論如何,它確實被執(zhí)行了。幾個時鐘周期之后你的第一條微指令前往端口2,該端口是讀單元(Load Address地址 execution unit)。剩余的微指令一直等待,同時各個端口正在收集不同的微指令。他們都在等待端口 2 將數(shù)據(jù)從緩存和內(nèi)存中加載進來并放在臨時存儲空間內(nèi)。
他們等了很久……
相當久的時間……
不過在他們等待第一條微指令返回數(shù)據(jù)的時候,又有其他的新指令又進來。好在處理器知道如何讓這些指令亂序執(zhí)行(即后到達保留站的微指令被優(yōu)先執(zhí)行)。
當?shù)谝粭l微指令返回了數(shù)據(jù),剩余的兩條微指令被立即送往執(zhí)行端口 0 和1.現(xiàn)在這 4 條微指令都已經(jīng)運行,最終它們會返回保留站。
這些微指令返回后交出他們的“票”并給出各自的臨時地址。通過這些地址,你作為一個完整的指令,將他們合并。最后 CPU 將結(jié)果交給你并使你退出
當你到達標有“退出”的門的時候,你會發(fā)現(xiàn)這里要排一個隊列。你進入后發(fā)現(xiàn)你剛好站在你前面進來指令的后面,即使執(zhí)行中的順序可能已經(jīng)不同,但你們退出的順序繼續(xù)保持一致??磥韥y序執(zhí)行部件真正知道自己做了什么。
每條指令最終離開 CPU,每次一條指令,就和指令指針指向的順序一樣!
結(jié)論
希望這篇小文能夠給讀者展示一些處理器工作的奧秘,要知道,這并不是魔術(shù)。
讓我們回到最初的問題,現(xiàn)在我們應(yīng)該可以給出一些較好的答案了。
處理器內(nèi)部是如何工作的呢?在這個復(fù)雜的過程中,指令首先被分解為更小的微指令命令,這些微指令以亂序的方式盡可能快的被執(zhí)行,然后按照原始的順序提交執(zhí)行結(jié)果。因此,從外部看來,所有的指令都是按照順序的方式執(zhí)行的。但是現(xiàn)在我們知道,處理器內(nèi)部是以亂序的方式處理指令的,有時甚至以猜測的方式來運行分支代碼。
運行一條指令究竟需要多長時間呢?對于沒有使用流水線技術(shù)的處理器來說,這是一個容易回答的問題,但對于現(xiàn)代的處理器來說,一條指令的執(zhí)行時間與它周圍指令的內(nèi)容以及臨近 cache 的大小和內(nèi)容都有關(guān)。一條指令通過處理器有一個最小的時間,但只能粗略的說這個時間是恒定的。一個好的程序員和編譯器可以讓很多條指令同時運行,從而使每條指令的分攤時間幾乎為零。這里說的幾乎為零的執(zhí)行時間并不是指一條指令的總的執(zhí)行時間很短,相反,通過整個亂序部件和等待內(nèi)存讀寫數(shù)據(jù)是需要花費很多時間的。
一個新的處理器擁有 12 級或者 18 級、甚至更深的 31 級流水線意味著什么呢?這意味著更多的指令可以被同時送進加工廠。一個非常深的流水線可以讓幾百條指令同時被處理。當一切順利時,一個亂序部件可以保持高速運轉(zhuǎn),從而獲得驚人的吞吐量。不幸的是,深的流水線同時意味著流水線停頓會從一個相對可以容忍的性能損失變成一個可怕的性能噩夢。因為幾百條指令都不得不停頓下來,等待流水線恢復(fù)運轉(zhuǎn)。
我怎么根據(jù)這些信息來優(yōu)化程序呢?幸運的是,CPU 可以在大部分常見情況下工作良好,并且編譯器已經(jīng)為亂序處理器優(yōu)化了近 20 年。當指令和數(shù)據(jù)按照順序(沒有煩人的跳轉(zhuǎn))執(zhí)行時,CPU 可以獲得最好的性能。因此,首先,使用簡單的代碼。簡單直接的代碼會幫助編譯器的優(yōu)化引擎識別并優(yōu)化代碼。盡量不使用跳轉(zhuǎn)指令,當你不得不跳轉(zhuǎn)時,盡量每次跳轉(zhuǎn)到同樣的方向。復(fù)雜的設(shè)計,例如動態(tài)跳轉(zhuǎn)表,雖然看起來很酷并且的確可以完成非常強大的功能,但不管是處理器還是編譯器,都無法進行很好的預(yù)測處理,因此復(fù)雜的代碼很可能導致流水線停頓和猜測錯誤,從而極大的損害處理器性能。其次,使用簡單的數(shù)據(jù)結(jié)構(gòu)。保持數(shù)據(jù)順序、相鄰和連續(xù)可以阻止數(shù)據(jù)停頓。使用正確的數(shù)據(jù)結(jié)構(gòu)和數(shù)據(jù)分布可以獲得很大的性能提升。只要保持代碼和數(shù)據(jù)結(jié)構(gòu)盡量簡單,剩下的工作就可以放心地交給編譯器的優(yōu)化引擎來完成了。