關(guān)于現(xiàn)代CPU,程序員應(yīng)當(dāng)更新的知識(shí)
有人在Twitter上談到了自己對(duì)CPU的認(rèn)識(shí):
我記憶中的CPU模型還停留在上世紀(jì)80年代:一個(gè)能做算術(shù)、邏輯、移位和位操作,可以加載,并把信息存儲(chǔ)在記憶體中的盒子。我隱約意識(shí)到了各種新發(fā)展,例如矢量指令(SIMD),新CPU還擁有了虛擬化支持(雖然不知道這在實(shí)際使用中意味著什么)。
我錯(cuò)過了哪些很酷的發(fā)展呢?有什么是今天的CPU可以做到而去年還做不到的呢?那兩年,五年或者十年之前的CPU又如何呢?我最感興趣的事是,哪些程序員需要自己動(dòng)手才能充分利用的功能(或者不得不重新設(shè)計(jì)編程環(huán)境)。我想,這不該包括超線程/SMT,但我并不確定。我也對(duì)暫時(shí)CPU做不到但是未來可以做得到的事感興趣。
本文內(nèi)容除非另有說明,都是指在x86和Linux環(huán)境下。歷史總在重演,很多x86上的新事物,對(duì)于超級(jí)計(jì)算機(jī)、大型機(jī)和工作站來說已經(jīng)是老生常談了。
現(xiàn)狀
雜記
現(xiàn)代CPU擁有更寬的寄存器,可尋址更多內(nèi)存。在上世紀(jì)80年代,你可能已經(jīng)使用過8位CPU,但現(xiàn)在肯定已在使用64位CPU。除了能提供更多地址空間,64位模式(對(duì)于32位和64位操作通過x867浮點(diǎn)避免偽隨機(jī)地獲得80位精度)提供了更多寄存器和更一致的浮點(diǎn)結(jié)果。自80年代初已經(jīng)被引入x86的其他非常有可能用到的功能還包括:分頁/虛擬內(nèi)存,pipelining和浮點(diǎn)運(yùn)算。
本文將避免討論那些寫驅(qū)動(dòng)程序、BIOS代碼、做安全審查,才會(huì)用到的不尋常的底層功能,如APIC/x2APIC,SMM或NX位等。
內(nèi)存/緩存 (Memory / Caches)
在所有話題中,最可能真正影日常編程工作的是內(nèi)存訪問。我的***臺(tái)電腦是286在,那臺(tái)機(jī)器上,一次內(nèi)存訪問可能只需要幾個(gè)時(shí)鐘周期。幾年前,我使用奔騰4,內(nèi)存訪問需要花費(fèi)超過400時(shí)鐘周期。處理器比內(nèi)存的發(fā)展速度快得多,對(duì)于內(nèi)存較慢問題的解決方法是增加緩存,如果訪問模式可被預(yù)測(cè),常用數(shù)據(jù)訪問速度更快,還有預(yù)取——預(yù)加載數(shù)據(jù)到緩存。
幾個(gè)周期與400多個(gè)相比,聽起來很糟——慢了100倍。但一個(gè)對(duì)64位(8字節(jié))值塊讀取并操作的循環(huán),CPU聰明到能在我需要之前就預(yù)取正確的數(shù)據(jù),在3Ghz處理器上,以約22GB/s的速度處理,我們只丟了8%的性能而不是100倍。
通過使用小于CPU緩存的可預(yù)測(cè)內(nèi)存訪問模式和數(shù)據(jù)塊操作,在現(xiàn)代CPU緩存架構(gòu)中能發(fā)揮***優(yōu)勢(shì)。如果你想盡可能高效,這份文件是個(gè)很好的起點(diǎn)。消化了這100頁P(yáng)DF文件后,接下來,你會(huì)想熟悉系統(tǒng)的微架構(gòu)和內(nèi)存子系統(tǒng),以及學(xué)習(xí)使用類似likwid這樣的工具來分析和測(cè)驗(yàn)應(yīng)用程序。
TLBs
芯片里也有小緩存來處理各種事務(wù),除非需要全力實(shí)現(xiàn)微優(yōu)化,你并不需要知道解碼指令緩存和其他有趣的小緩存。***的例外是TLB——虛擬內(nèi)存查找緩存(通過x86上4級(jí)頁表結(jié)構(gòu)完成)。頁表在L1數(shù)據(jù)緩存,每個(gè)查詢有4次,或16個(gè)周期來進(jìn)行一次完整的虛擬地址查詢。對(duì)于所有需要被用戶模式內(nèi)存訪問的操作來說,這是不能接受的,從而有了小而快的虛擬地址查找的緩存。
因?yàn)?**級(jí)TLB緩存必須要快,被嚴(yán)重地限制了尺寸。如果使用4K頁面,確定了在不發(fā)生TLB丟失的情況下能找到的內(nèi)存數(shù)量。x86還支持2MB和1GB頁面;有些應(yīng)用程序會(huì)通過使用較大頁面受益匪淺。如果你有一個(gè)長時(shí)間運(yùn)行,且使用大量內(nèi)存的應(yīng)用程序,很值得研究這項(xiàng)技術(shù)的細(xì)節(jié)。
亂序執(zhí)行/序列化 (Out of Order Execution / Serialization)
最近二十年,x86芯片已經(jīng)能思考執(zhí)行的次序(以避免因?yàn)橐粋€(gè)停滯資源而被阻塞)。這有時(shí)會(huì)導(dǎo)致很奇怪的表現(xiàn)。x86非常嚴(yán)格的要求單一CPU,或者外部可見的狀態(tài),像寄存器和記憶體,如果每件事都在按照順序執(zhí)行都必須及時(shí)更新。
這些限制使得事情看起來像按順序執(zhí)行,在大多數(shù)情況下,你可以忽略O(shè)oO(亂序)執(zhí)行的存在,除非要竭力提高性能。主要的例外是,你不僅要確保事情在外部看起來像是按順序執(zhí)行,實(shí)際上在內(nèi)部也要真的按順序。
一個(gè)你可能關(guān)心的例子是,如果試圖用rdtsc測(cè)量一系列指令的執(zhí)行時(shí)間,rdtsc將讀出隱藏的內(nèi)部計(jì)數(shù)器并將結(jié)果置于edx和eax這些外部可見的寄存器。
假設(shè)我們這樣做:
- foo
- rdtsc
- bar
- mov %eax, [%ebx]
- baz
其中,foo,bar和baz不去碰eax,edx或[%ebx]。跟著rdtsc的mov會(huì)把eax值寫入內(nèi)存某個(gè)位置,因?yàn)閑ax外部可見,CPU將保證rdtsc執(zhí)行后mov才會(huì)執(zhí)行,讓一切看起來按順序發(fā)生。
然而,因?yàn)閞dtsc,foo或bar之間沒有明顯的依賴關(guān)系 ,rdtsc可能在foo之前,在foo和bar之間 ,或在bar之后。甚至只要baz不以任何方式影響移mov,令也可能存在baz在rdtsc之前執(zhí)行的情況。有些情況下這么做沒問題,但如果rdtsc被用來衡量foo的執(zhí)行時(shí)間就不妙了。
為了精確地安排rdtsc和其他指令的順序,我們需要串行化所有執(zhí)行。如何準(zhǔn)確的做到?請(qǐng)參考英特爾的這份文檔。
內(nèi)存/并發(fā) (Memory / Concurrency)
上面提到的排序限制意味著相同位置的加載和存儲(chǔ)彼此間不能被重新排序,除此以外,x86加載和存儲(chǔ)有一些其他限制。特別是,對(duì)于單一CPU,不管是否是在相同的位置,存儲(chǔ)不會(huì)與之前的負(fù)載一起被記錄。
然而,負(fù)載可以與更早的存儲(chǔ)一起被記錄。例如:
- mov 1, [%esp]
- mov [%ebx], %eax
執(zhí)行起來就像:
- mov [%ebx], %eax
- mov 1, [%esp]
但反之則不然——如果你寫了后者,它永遠(yuǎn)不能像你前面寫那樣被執(zhí)行。
你可能通過插入串行化指令迫使前一個(gè)實(shí)例像寫起來一樣來執(zhí)行。但是這需要CPU序列化所有指令這會(huì)非常緩慢,因?yàn)樗仁笴PU要等到所有指令完成串行化后才能執(zhí)行任何操作。如果你只關(guān)心加載/存儲(chǔ)順序,另外還有一個(gè) mfence指令只用于序列化加載和存儲(chǔ)。
本文不打算討論memory fence,lfence和sfence,但你可以在這里閱讀更多關(guān)于它們的內(nèi)容 。
單核加載和存儲(chǔ)大多是有序的,對(duì)于多核,上述限制同樣適用;如果core0在觀察core1,就可以看到所有的單核規(guī)則適用于core1的加載和存儲(chǔ)。然而如果core0和core1相互作用,不能保證它們的相互作用也是有序的。
例如,core0和core1通過設(shè)置為0的eax和edx開始,core0執(zhí)行:
- mov 1, [_foo]
- mov [_foo], %eax
- mov [_bar], %edx
而core1執(zhí)行
- mov 1, [_bar]
- mov [_bar], %eax
- mov [_foo], %edx
對(duì)于這兩個(gè)核來說, eax必須是1,因?yàn)?**指令和第二指令相互依賴。然而,eax有可能在兩個(gè)核里都是0,因?yàn)閏ore0的第三行可能在core1沒看到任何東西時(shí)執(zhí)行,反之亦然。
memory barriers序列化一個(gè)核心內(nèi)的存儲(chǔ)器訪問。Linus對(duì)于使用memory barriers而不是使用locking有這樣一段話 :
不用locking的真正代價(jià)最終不可避免。通過使用memory barriers自以為聰明的做事幾乎總是錯(cuò)誤的前奏。在所有可以發(fā)生在十多種不同架構(gòu)并且有著不同的內(nèi)存排序的情況下,缺失一個(gè)小小的barrier真的很難讓你理清楚…事實(shí)上,任何時(shí)候任何人編了一個(gè)新的鎖定機(jī)制,他們總是會(huì)把它弄錯(cuò)。
而事實(shí)證明,在現(xiàn)代的x86處理器上,使用locking來實(shí)現(xiàn)并發(fā)通常比使用memory barriers代價(jià)低,所以讓我們來看看鎖。
如果設(shè)置_foo為0,并有兩個(gè)線程執(zhí)行incl (_foo)10000次——一個(gè)單指令同一位置遞增20000次,但理論上結(jié)果可能2。搞清楚這一點(diǎn)是個(gè)很好的練習(xí)。
我們可以用一段簡單的代碼試驗(yàn):
- #include <stdlib.h>
- #include <thread>
- #define NUM_ITERS 10000
- #define NUM_THREADS 2
- int counter = 0;
- int *p_counter = &counter;
- void asm_inc() {
- int *p_counter = &counter;
- for (int i = 0; i < NUM_ITERS; ++i) {
- __asm__("incl (%0) \n\t" : : "r" (p_counter));
- }
- }
- int main () {
- std::thread t[NUM_THREADS];
- for (int i = 0; i < NUM_THREADS; ++i) {
- t[i] = std::thread(asm_inc);
- }
- for (int i = 0; i < NUM_THREADS; ++i) {
- t[i].join();
- }
- printf("Counter value: %i\n", counter);
- return 0;
- }
用clang++ -std=c++11 –pthread在我的兩臺(tái)機(jī)器上編譯得到的分布結(jié)果如下:
不僅得到的結(jié)果在運(yùn)行時(shí)變化,結(jié)果的分布在不同的機(jī)器上也是不同。我們永遠(yuǎn)沒到理論上最小的2,或就此而言,任何低于10000的結(jié)果,但有可能得到10000和20000之間的最終結(jié)果。
盡管incl是個(gè)單獨(dú)的指令,但不能保證原子性。在內(nèi)部,incl是后面跟一個(gè)add后再跟一個(gè)存儲(chǔ)的負(fù)載。在cpu0里的一個(gè)增加有可能偷偷的溜進(jìn)cpu1里面的負(fù)載和存儲(chǔ)之間執(zhí)行,反之亦然。
英特爾對(duì)此的解決方案是少量的指令可以加lock前綴,以保證它們的原子性。如果我們把上面代碼的incl改成lock incl,輸出始終是20000。
為了使序列有原子性,我們可以使用xchg或cmpxchg, 它們始終被鎖定為比較和交換的基元。本文不會(huì)詳細(xì)描它是如何工作的,但如果你好奇可以看這篇David Dalrymple的文章。
為了使存儲(chǔ)器的交流原子性,lock相對(duì)于彼此在global是有序的,而且加載和存儲(chǔ)對(duì)于鎖不會(huì)被重新排序相。對(duì)于內(nèi)存排序嚴(yán)格的模型,請(qǐng)參考x86 TSO文檔。
在C或C++中:
- local_cpu_lock = 1;
- // .. 做些重要的事 ..
- local_cpu_lock = 0;
編譯器不知道local_cpu_lock = 0不能被放在重要的中間部分。Compiler barriers與CPU memory barriers不同。由于x86內(nèi)存模型是比較嚴(yán)格,一些編譯器的屏障在硬件層面是選擇不作為,并告訴編譯器不要重新排序。如果使用的語言比microcode,匯編,C或C++抽象層級(jí)高,編譯器很可能沒有任何類型的注釋。
#p#
內(nèi)存/移植 (Memory / Porting)
如果要把代碼移植到其他架構(gòu),需要注意的是,x86也許有著今天你能遇到的任何架構(gòu)里***的內(nèi)存模式。如果不仔細(xì)思考,它移植到有較弱擔(dān)保的架構(gòu)(PPC,ARM,或Alpha),幾乎肯定得到報(bào)錯(cuò)。
考慮Linus對(duì)這個(gè)例子的評(píng)論:
- CPU1 CPU2
- ---- ----
- if (x == 1) z = y;
- y = 5; mb();
- x = 1;
…如果我讀了Alpha架構(gòu)內(nèi)存排序保證正確,那么至少在理論上,你真的可以得到Z = 5
mb是memory barrier(內(nèi)存屏障)。本文不會(huì)細(xì)講,但如果你想知道為什么有人會(huì)建立這樣一個(gè)允許這種瘋狂行為發(fā)生的規(guī)范,想一想成產(chǎn)成本上升打垮DEC之前,其芯片快到可以在相同的基準(zhǔn)下通過仿真運(yùn)行卻比x86更快。對(duì)于為什么大多數(shù)RISC-Y架構(gòu)做出了當(dāng)時(shí)的決定請(qǐng)參見關(guān)于Alpha架構(gòu)背后動(dòng)機(jī)的論文。
順便說一句,這是我很懷疑Mill架構(gòu)的主要原因。暫且不論關(guān)于是否能達(dá)到他們號(hào)稱的性能,僅僅在技術(shù)上出色并不是一個(gè)合理的商業(yè)模式。
內(nèi)存/非臨時(shí)存儲(chǔ)/寫結(jié)合存儲(chǔ)器 (Memory / Non-Temporal Stores / Write-Combine Memory)
上節(jié)所述的限制適用于可緩存(即“回寫(write-back)”或WB)存儲(chǔ)器。在此之前,只有不可緩存(UC)內(nèi)存。
一個(gè)關(guān)于UC內(nèi)存有趣的事情是,所有加載和存儲(chǔ)都被設(shè)計(jì)希望能在總線上加載或存儲(chǔ)。對(duì)于沒有緩存或者幾乎沒有板載緩存的處理器,這么做完全合理。
內(nèi)存/NUMA
非一致內(nèi)存訪問(NUMA),即對(duì)于不同處理器來說,內(nèi)存訪問延遲和帶寬各有不同。因?yàn)镹UMA或ccNUMA如此普遍,以至于是被默認(rèn)為采用的。
這里要求的是共享內(nèi)存的線程應(yīng)該在同一個(gè)socket上,內(nèi)存映射I/O重線程應(yīng)該確保它與最接近的I/O設(shè)備的socket對(duì)話。
曾幾何時(shí),只有內(nèi)存。然后CPU相對(duì)于內(nèi)存速度太快以致于人們想增加一個(gè)緩存。緩存與后備存儲(chǔ)器(內(nèi)存)不一致是一個(gè)壞消息,因此緩存必須保持它堅(jiān)持著什么的信息,所以它才知道是否以及何時(shí)它需要向后備存儲(chǔ)寫東西。
這不算太糟糕,而一旦你獲得了兩個(gè)有自己緩存的核心,情況就變復(fù)雜了。為了保持作為無緩存的情況下相同的編程模型,緩存必須相互之間以及與后備存儲(chǔ)器是一致的。由于現(xiàn)有的加載/存儲(chǔ)指令在其API中沒有什么允許他們說“對(duì)不起!這個(gè)加載因?yàn)閯e的cpu在使用你想用的地址而失敗了” ,最簡單的方式是讓每個(gè)CPU每次要加載或存儲(chǔ)東西的時(shí)候發(fā)一個(gè)信息到總線上。我們已經(jīng)有了這個(gè)兩個(gè)CPU都可以連接的內(nèi)存總線,所以只要要求另一個(gè)CPU在其數(shù)據(jù)緩存有修改時(shí)做出回復(fù)(并失去相應(yīng)的緩存行)。
在大多數(shù)情況下,每個(gè)CPU只涉及其他CPU不關(guān)心的數(shù)據(jù),所以有一些浪費(fèi)的總線流量。但不算糟糕,因?yàn)橐坏〤PU拿出一條消息說“你好!我要占有這個(gè)地址并修改數(shù)據(jù)”,可以假定在其他的CPU要求前完全擁有該地址,雖然不是總會(huì)發(fā)生。
對(duì)于4核CPU,依然可以工作,雖然字節(jié)浪費(fèi)相比有點(diǎn)多。但其中每個(gè)CPU對(duì)其他每一個(gè)CPU的響應(yīng)失敗比例遠(yuǎn)遠(yuǎn)超出4個(gè)CPU總和,既因?yàn)榭偩€被飽和,也因?yàn)榫彺鎸⒌玫斤柡停ň彺娴奈锢沓叽?成本是以同時(shí)的讀和寫數(shù)量 O(n^2) ,并且速度與大小負(fù)相關(guān))。
這個(gè)問題“簡單”的解決方法是有一個(gè)單獨(dú)的集中目錄記錄所有的信息,而不是做N路的對(duì)等廣播。反正因?yàn)楝F(xiàn)在我們正在一個(gè)芯片上包2-16個(gè)內(nèi)核,每個(gè)芯片(socket)對(duì)每個(gè)核的緩存狀態(tài)有個(gè)單一目錄跟蹤是很自然的事。
不僅解決了每個(gè)芯片的問題,而且需要通過某種方式讓芯片相互交談。不幸的是,當(dāng)我們擴(kuò)展這些系統(tǒng)即使對(duì)于小型系統(tǒng)總線速度也快到真的很難驅(qū)動(dòng)一個(gè)信號(hào)遠(yuǎn)到連接一堆芯片和都在一條總線上的記憶體。最簡單的解決辦法就是讓每個(gè)插座都擁有一個(gè)存儲(chǔ)器區(qū)域,所以每一個(gè)socket并不需要被連接到的存儲(chǔ)器每一個(gè)部分。因?yàn)樗苊鞔_哪個(gè)目錄擁有特定的一段內(nèi)存,這也避免了目錄需要一個(gè)更高級(jí)別的目錄的復(fù)雜性。
這樣做的缺點(diǎn)是,如果占用一個(gè)socket并且想要一些被別的socket擁有的memory,會(huì)有顯著的性能損失。為簡單起見,大多數(shù)“小”(<128核)系統(tǒng)使用環(huán)形總線,因此性能損失的不僅僅是通過一系列跳轉(zhuǎn)達(dá)到memory付出的直接延遲/帶寬處罰,他也用光了有限的資源(環(huán)狀總線)和減慢了其他socekt的訪問速度。
理論上來講,OS會(huì)透明處理,但往往低效 。
Context Switches/系統(tǒng)調(diào)用(Syscalls)
在這里,syscall是指Linux的系統(tǒng)調(diào)用,而不是x86的SYSCALL或者SYSENTER指令。
所有現(xiàn)代處理器具有一個(gè)副作用是,Context Switches代價(jià)昂貴,這會(huì)導(dǎo)致系統(tǒng)調(diào)用代價(jià)高昂。Livio Soares和Michael Stumm的論文對(duì)此做了詳細(xì)討論。我在下文將用一些他們的數(shù)據(jù)。下圖為Xalan上的酷睿i7每一個(gè)時(shí)鐘可以多少指令(IPC):
系統(tǒng)調(diào)用的14000周期后,代碼仍不是全速運(yùn)行。
下面是幾個(gè)不同的系統(tǒng)調(diào)用的足跡表,無論是直接成本(指令和周期),還是間接成本(緩存和TLB驅(qū)逐的數(shù)量)。
有些系統(tǒng)調(diào)用引起了40多次的TLB回收!對(duì)于具有64項(xiàng)D-TLB的芯片,幾乎掃蕩光了TLB。緩存回收不是毫無代價(jià)。
系統(tǒng)調(diào)用的高成本是人們對(duì)于高性能的代碼轉(zhuǎn)而進(jìn)行使用腳本化的系統(tǒng)調(diào)用(例如epoll, 或者recvmmsg)究其原因,人們需要高性能I/O經(jīng)常使用用戶空間的I/O stack。Context Switches的成本就是為什么高性能的代碼往往是一個(gè)核心一個(gè)線程(甚至是固定線程上一個(gè)單線程),而不是每個(gè)邏輯任務(wù)一個(gè)線程的原因。
這種高代價(jià)也是VDSO在后面驅(qū)動(dòng),把一些簡單的不需要任何升級(jí)特權(quán)的系統(tǒng)調(diào)用放進(jìn)簡單的用戶空間庫調(diào)用。
SIMD
基本上所有現(xiàn)代的x86 CPU都支持SSE,128位寬的向量寄存器和指令。因?yàn)橐瓿啥啻蜗嗤牟僮骱艹R?,英特爾增加了指令,可以讓你像?個(gè)64位塊一樣對(duì)128位數(shù)據(jù)塊操作,或者4個(gè)32位的塊,8個(gè)16位塊等。ARM用不同的名字(NEON)支持同樣的事情,而且支持的指令也很相似。
通過使用SIMD指令獲得了2倍,4倍加速這是很常見的,如果你已經(jīng)有了一個(gè)計(jì)算繁重的工作這絕對(duì)值得期待。
編譯器足夠到可以分辨常見的可以實(shí)現(xiàn)矢量化模式的簡單的代碼,就像下面代碼,會(huì)自動(dòng)使用現(xiàn)代編譯器的向量指令:
- for (int i = 0; i < n; ++i) {
- sum += a[i];
- }
但是,如果你不手寫匯編語言,編譯器經(jīng)常會(huì)產(chǎn)生非優(yōu)化的代碼 ,特別是對(duì)SIMD代碼,所以如果你很關(guān)心盡可能的得到***性能,你就要看看反匯編并檢查你編譯器的優(yōu)化錯(cuò)誤。
電源管理
有現(xiàn)代CPU都有很多花哨的電源管理功能用來在不同的場景優(yōu)化電源使用。這些的結(jié)果是“跑去閑置”,因?yàn)楸M可能快的完成工作,然后讓CPU回去睡覺是最節(jié)能的方式。
盡管有很多做法已經(jīng)被證明進(jìn)行特定的微優(yōu)化可以對(duì)電源消耗有利,但把這些微優(yōu)化應(yīng)用在實(shí)際的工作負(fù)載中通常會(huì)比預(yù)期的收益小 。
GPU/GPGPU
相比其他部分我不是很夠資格來談?wù)撨@些。幸運(yùn)的是,Cliff Burdick自告奮勇地寫了下面這節(jié):
2005年之前,圖形處理單元(GPU)被限制在一個(gè)只允許非常有限硬件控制量的API。由于庫變得更加靈活,程序員開始使用處理器處理更常用的任務(wù),如線性代數(shù)例程。GPU的并行架構(gòu)可以通過發(fā)射數(shù)百并發(fā)線程在大量的矩陣塊中工作。然而,代碼必須使用傳統(tǒng)的圖形API,并仍被限制于可以控制多少硬件。Nvidia和ATI注意到了這點(diǎn)并發(fā)布了可以使顯卡界外的人更熟悉的API來獲得更多的硬件訪問的框架。該庫得到了普及,今天的GPU同CPU一起被廣泛用于高性能計(jì)算(HPC)。
相比于處理器,GPU硬件主要有幾個(gè)差別,概述如下:
處理器
在頂層,一個(gè)GPU處理器包含一個(gè)或多個(gè)數(shù)據(jù)流多重處理器(SMs)?,F(xiàn)代GPU的每個(gè)流的多重理器通常包含超過100個(gè)浮點(diǎn)單元,或在GPU的世界通常被稱為核。每個(gè)核心通常主頻在800MHz左右,雖然像CPU一樣,具有更高的時(shí)鐘頻率但較少內(nèi)核的處理器也存在。GPU的處理器缺乏自己同行CPU的許多特色,包括更大的緩存和分支預(yù)測(cè)。在核的不同層,SMs,和整體處理器之間,通訊變得越來越慢。出于這個(gè)原因,在GPU上表現(xiàn)良好的問題通常是高度平行的,但有一些數(shù)據(jù)能夠在小數(shù)目的線程間共用。我們將在下面的內(nèi)存部分解釋為什么。
內(nèi)存(Memory)
現(xiàn)代GPU內(nèi)存被分為3類:全局內(nèi)存,共享內(nèi)存和寄存器。全局存儲(chǔ)器是GDDR通常GPU盒子上廣告宣稱約為2-12GB大小,并具有通過300-400GB /秒的速度。全局存儲(chǔ)器在處理器上的所有SMS所有線程都能被訪問,并且也是內(nèi)存卡上最慢的類型。共享內(nèi)存,正如其名所指,是同一個(gè)SM中的所有線程之間共享內(nèi)存。它通常至少是全局儲(chǔ)蓄器兩倍的速度,但對(duì)不同SM的線程之間是不被允許進(jìn)行訪問的。寄存器很像在CPU上的寄存器,他們是GPU上訪問數(shù)據(jù)最快的方式,但它們只在每個(gè)本地線程,數(shù)據(jù)對(duì)于其他正在運(yùn)行的不同線程是不可見的。共享內(nèi)存和全局內(nèi)存對(duì)他們?nèi)绾文軌虮辉L問都有很嚴(yán)格的規(guī)定,對(duì)不遵守這些規(guī)則的行為有嚴(yán)重性能下降的處罰。為了達(dá)到上述吞吐量,內(nèi)存訪問必須在同線程組間線程之間完整的合并。類似于CPU讀入一個(gè)單一的緩存行,如果對(duì)齊合適的話,GPU對(duì)于單一的訪問可以有緩存行可以服務(wù)一個(gè)組里的所有線程。然而,最壞的狀況是一組里所有線程訪問不同的緩存行,每個(gè)線程都要求一個(gè)獨(dú)立的記憶體讀。這通常意味著緩存行中的數(shù)據(jù)不被線程使用,并且存儲(chǔ)器的可用吞吐量下降。類似的規(guī)則同樣適用于共享內(nèi)存,有一些例外,我們將不在這里涵蓋。
線程模型 (Threading Model)
GPU線程在一個(gè)單指令多線程(SIMT)方式下運(yùn)行,并且每個(gè)線程以組的形式在硬件中以預(yù)定義大?。ㄍǔ?2)運(yùn)行。這***一部分有很多的影響;該組中的每個(gè)線程必須同一時(shí)間在同一指令下工作。如果任何一組中的線程的需要從他人那里獲得代碼的發(fā)散路徑(例如一個(gè)if語句)的代碼,所有不參與該分支的線程會(huì)到該分支結(jié)束才能開始。作為一個(gè)簡單的例子:
- if (threadId < 5) {
- // Do something
- }
- // Do More
在上面的代碼中,這個(gè)分支會(huì)導(dǎo)致我們的32個(gè)線程中的27組暫停執(zhí)行,直到分支結(jié)束。你可以想象,如果多組線程運(yùn)行這段代碼,整體性能會(huì)因大部分的內(nèi)核處于閑置狀態(tài)將受到很大打擊。只有當(dāng)線程整組被鎖定才能使硬件允許交換另外一組的核來運(yùn)行。
接口(Interfaces)
現(xiàn)代GPU必須有一個(gè)CPU同CPU和GPU內(nèi)存之間進(jìn)行數(shù)據(jù)復(fù)制的發(fā)送和接收,并啟動(dòng)GPU并且編碼。在***吞吐量的情況下,一個(gè)有著16個(gè)通道的PCIe 3.0總線可達(dá)到約13-14GB / s的速度。這可能聽起來很高,但相對(duì)于存在GPU本身的內(nèi)存速度,他們慢了一個(gè)數(shù)量級(jí)。事實(shí)上,圖形處理器變得更強(qiáng)大以致于PCIe總線日益成為一個(gè)瓶頸。為了看到任何GPU超過CPU的性能優(yōu)勢(shì),GPU的必須裝有大量的工作,以使GPU需要運(yùn)行的工作的時(shí)間遠(yuǎn)遠(yuǎn)的高于數(shù)據(jù)發(fā)送與接收的時(shí)間。
較新的GPU具備一些功能可以動(dòng)態(tài)的在GPU代碼里分配工作而不需要再回到CPU推出的GPU代碼中動(dòng)態(tài)的工作,而無需返回到CPU,單目前他的應(yīng)用相當(dāng)有局限性。
GPU結(jié)論
由于CPU和GPU之間主要的架構(gòu)差異,很難想象任何一個(gè)完全取代另一個(gè)。事實(shí)上,GPU很好的補(bǔ)充了CPU的并行工作,使CPU可以在GPU運(yùn)行時(shí)獨(dú)立完成其他任務(wù)。AMD公司正在試圖通過他們的“非均相體系結(jié)構(gòu)”(HSA)合并這兩種技術(shù),但用現(xiàn)有的CPU代碼,并決定如何將處理器的CPU和GPU部分分割開來將是一個(gè)很大的挑戰(zhàn),不僅僅對(duì)于處理器來說,對(duì)于編譯器也是。
虛擬化
除非你正在編寫非常低級(jí)的代碼直接處理虛擬化,英特爾植入的虛擬化指令通常不是你需要思考的問題。
同那些東西打交道相當(dāng)混亂,可以從這里的代碼看到。即使對(duì)于那里展示的非常簡單的例子,設(shè)置起用Intel的VT指令來啟動(dòng)一個(gè)虛擬客戶端也需要大約1000行低階代碼。
虛擬內(nèi)存
如果你看一下Vish的VT代碼,你會(huì)發(fā)現(xiàn)有一塊很好的代碼專門用于頁表/虛擬內(nèi)存。這是另一個(gè)除非你正在編寫操作系統(tǒng)或其他低級(jí)別的系統(tǒng)代碼你不必?fù)?dān)心的“新”功能。使用虛擬內(nèi)存比使用分段存儲(chǔ)器更簡單,但本文暫且討論到這里。
SMT/超線程 (Hyper-threading)
超線程對(duì)于程序員來說大部分是透明的。一個(gè)典型的在單核上啟用SMT的增速是25%左右。對(duì)于整體吞吐量來說是好的,但它意味著每個(gè)線程可能只能獲得其原有性能的60%。對(duì)于您非常關(guān)心單線程性能的應(yīng)用程序,你可能***禁用SMT。雖然這在很大程度上取決于工作量,而且對(duì)于任何其他的變化,你應(yīng)該在你的具體工作負(fù)載運(yùn)行一些基準(zhǔn)測(cè)試,看看有什么效果***。
所有這些復(fù)雜性添加到芯片(和軟件)的一個(gè)副作用是性能比曾經(jīng)預(yù)期的要少了很多;對(duì)特定硬件基準(zhǔn)測(cè)試的重要性相對(duì)應(yīng)的有所回升。
人們常常用“計(jì)算機(jī)語言基準(zhǔn)游戲”作為證據(jù)來說一種語言比另一種速度更快。我試著自己重現(xiàn)的結(jié)果,用我的移動(dòng)Haswell(相對(duì)于在結(jié)果中使用的服務(wù)器Kentsfield),我得到的結(jié)果可以達(dá)到高達(dá)2倍的不同(相對(duì)速度)。即使在同一臺(tái)機(jī)器上運(yùn)行同一個(gè)基準(zhǔn),Nanthan Kurz 最近向我指出一個(gè)例子 gcc -O3 比 gcc –O2 慢25%改變對(duì)C ++程序的鏈接順序可導(dǎo)致15%的性能變化 。評(píng)測(cè)基準(zhǔn)的選定是個(gè)難題。
分行 (Branches)
傳統(tǒng)觀念認(rèn)為使用分支是昂貴的,并且應(yīng)該盡一切(大多數(shù))的可能避免。在Haswell上,分支的錯(cuò)誤預(yù)測(cè)代價(jià)是14個(gè)時(shí)鐘周期。分支錯(cuò)誤預(yù)測(cè)率取決于工作量。在一些不同的東西上使用 perf stat (bzip2,top,mysqld,regenerating my blog),我得到了在0.5%和4%之間的分支錯(cuò)誤預(yù)測(cè)率。如果我們假設(shè)一個(gè)正確的預(yù)測(cè)的分支費(fèi)用是1個(gè)周期,這個(gè)平均成本在.995 * 1 + .005 * 14 = 1.065 cycles to .96 * 1 + .04 * 14 = 1.52 cycles之間。這不是很糟糕。
從約1995年來這實(shí)際上夸大了代價(jià),由于英特爾加入條件移動(dòng)指令,使您可以在無需一個(gè)分支的情況下有條件地移動(dòng)數(shù)據(jù)。該指令曾被Linus批判的令人難忘的 ,這給了它一個(gè)不好的名聲,但是相比分支,使用cmos更有顯著的加速這是相當(dāng)普遍的額外分支成本的一個(gè)現(xiàn)實(shí)中的例子是使用整數(shù)溢出檢查。當(dāng)使用bzip2來壓縮一個(gè)特定的文件,那會(huì)增加約30%的指令數(shù)量(所有的增量從額外分支指令得來),這導(dǎo)致1%的性能損失 。
不可預(yù)知的分支是不好的,但大部分的分支是可以預(yù)見的。忽略分支的費(fèi)用直到你的分析器告訴你有一個(gè)熱點(diǎn)在如今是非常合理的。CPUs在過去十年中執(zhí)行優(yōu)化不好代碼方面變好了很多,而且編譯器在優(yōu)化代碼方面也變得更好,這使得優(yōu)化分支變成了不良的使用時(shí)間,除非你試圖在一些代碼中擠出絕對(duì)***表現(xiàn)。
如果事實(shí)證明這就是你所需要做的,你***還是使用檔案導(dǎo)引優(yōu)化而不是試圖手動(dòng)去搞這個(gè)東西。
如果你真的必須用手動(dòng)做到這一點(diǎn),有些編譯器指令你可以用來表示一個(gè)特定分支是否有可能被占用與否?,F(xiàn)代CPU忽略了分支提示說明,但它們可以幫助編譯器更好得布局代碼。
對(duì)齊 (Alignment)
經(jīng)驗(yàn)告訴我們應(yīng)該拉長struct,并確數(shù)據(jù)對(duì)齊。但在Haswell的芯片上,幾乎任何你能想到的任何不跨頁的單線程事情的誤配準(zhǔn)為零。有些情況下它是有用的,但在一般情況下,這是另一種無關(guān)緊要的優(yōu)化因?yàn)镃PU已經(jīng)變得在執(zhí)行不優(yōu)良代碼時(shí)好了很多。它無好處的增加了內(nèi)存占用的足跡也是有一點(diǎn)害處。
而且, 不要把事情頁面對(duì)齊或以其他方式排列到大的界限,否則會(huì)破壞緩存性能 。
自修改代碼 (Self-modifying code)
這是另外一個(gè)目前已經(jīng)不怎么有意義的優(yōu)化了。使用自修改代碼以減少代碼量或增加性能曾經(jīng)有意義,但由于現(xiàn)代的緩存傾向于拆分他們的L1指令和數(shù)據(jù)緩存,在一個(gè)芯片的L1緩存之間修改運(yùn)行的代碼需要昂貴的通信。
未來
下面是一些可能的變化,從最保守的推測(cè)到***膽的推測(cè)。
事務(wù)內(nèi)存和硬件鎖Elision (Transactional Memory and Hardware Lock Elision)
IBM已經(jīng)在他們自己的POWER芯片中有這些功能。英特爾嘗試著把這些東西加到Haswell,但因?yàn)橐粋€(gè)報(bào)錯(cuò)被禁用了。
事務(wù)內(nèi)存支持正如它聽起來這樣:事務(wù)的硬件支持。通過三個(gè)新的指令xbegin、xend和xabort。
xbegin開始一個(gè)新的事務(wù)。一個(gè)沖突(或xabort)使處理器(包括內(nèi)存)的架構(gòu)狀態(tài)回滾到在xbegin的狀態(tài)之前.如果您使用的是通過庫或語言支持的事務(wù)內(nèi)存,這對(duì)你來說應(yīng)該透明的。如果你正在植入庫支持,你就必須弄清楚如何將有有限的硬件緩沖區(qū)大小限制的硬件支持轉(zhuǎn)換成抽象的事務(wù)。
本文打算討論Elision硬件鎖,在本質(zhì)上,它被植入的機(jī)制與用于實(shí)現(xiàn)事務(wù)內(nèi)存的機(jī)制非常相似,而且它是被設(shè)計(jì)來加快基于鎖的代碼。如果你想利用HLE,看看這個(gè)文檔 。
快速I/O(Fast I/O)
對(duì)于存儲(chǔ)和網(wǎng)絡(luò)來說,I/O帶寬正在不斷上升,I/O延遲正在下降。問題是,I/O通常是通過系統(tǒng)調(diào)用完成。正如我們所看到的,系統(tǒng)調(diào)用的相對(duì)額外費(fèi)用一直在往上走。對(duì)于存儲(chǔ)和網(wǎng)絡(luò),答案是轉(zhuǎn)移到用戶模式的I/O堆棧。
黑硅(Dark Silicon)/系統(tǒng)級(jí)芯片
晶體管規(guī)?;粋€(gè)有趣的副作用是我們可以把很多晶體管包進(jìn)一個(gè)芯片上,但它們產(chǎn)生如此多的熱量,如果你不希芯片融化,普通晶體管大多數(shù)時(shí)間不能開關(guān)。
這樣做的結(jié)果把包括大量時(shí)間不使用的專用硬件變得更有意義。一方面,這意味著我們得到各種專用指令,如PCMP和ADX。但這也意味著,我們正把整個(gè)曾經(jīng)不集成在芯片上的設(shè)備與芯片集成。包括諸如GPU和(用于移動(dòng)設(shè)備)無線電。
與硬件加速的趨勢(shì)相結(jié)合,這也意味著企業(yè)設(shè)計(jì)自己的芯片,或者至少自己芯片的部分變得更有意義。通過收購PA Semi公司,蘋果公司已經(jīng)走出了很遠(yuǎn)。首先,加入少量定制的加速器給停滯不前的標(biāo)準(zhǔn)的ARM架構(gòu),然后添加自定義加速器給他們自己定制的架構(gòu)。由于正確的定制硬件和基準(zhǔn)和系統(tǒng)設(shè)計(jì)深思熟慮的結(jié)合,iPhone 4比我的旗艦級(jí)Android手機(jī)反應(yīng)還稍快,這個(gè)旗艦機(jī)比iPhone 4新了很多年,并且具有更快的處理器以及更大的內(nèi)存。
亞馬遜挑選了原Calxeda的團(tuán)隊(duì)的一部分,并雇用了一個(gè)足夠大小的硬件設(shè)計(jì)團(tuán)隊(duì)。Facebook也已經(jīng)挑選了ARM SoC的專家,并與高通公司在某些事情展開合作。Linus也有紀(jì)錄在案的發(fā)言,“我們將在各個(gè)方面看到更多的專用硬件” 等等。
結(jié)論
x86芯片已經(jīng)擁有了很多新的功能和非常有用的小特性。在大多數(shù)情況下,要利用這些優(yōu)勢(shì)你不需要知道它們具體是什么。真正的底層通常由庫或驅(qū)動(dòng)程序隱藏了起來,編譯器將嘗試照顧其余部分。例外是,如果你真的要寫底層代碼,這種情況下世界上已經(jīng)變得更加混亂,或者如果你想在你的代碼里獲得絕對(duì)的***表現(xiàn),就會(huì)更加怪異。
有些事似乎必然在未來發(fā)生。但過往的經(jīng)驗(yàn)卻又告訴我們,大多數(shù)的預(yù)測(cè)是錯(cuò)誤的,所以誰又知道呢?