GPU體系架構(一):數據的並行處理

最近在瞭解GPU架構這方面的內容,因爲資料零零散散,因此準備寫兩篇博客整理一下。GPU的架構複雜無比,這兩篇文章也是從宏觀的層面去一窺GPU的工做原理罷了緩存

 

GPU根據廠商的不一樣,顯卡型號的不一樣,GPU的架構也有差異,可是大致的設計基本相同,原理的部分也是相通的。下面咱們就以NVIDIA的Fermi架構爲藍本,從下降延遲的角度,來說解一下GPU究竟是如何利用數據的並行處理來提高性能的。有關GPU的架構細節和邏輯管線的實現細節,咱們將在下一篇裏再講。架構

 

不管是CPU仍是GPU,都在使用各類各樣的策略來避免停滯(stall)。app

 

CPU的優化路線有不少,包括使用pipeline,提升主頻,在芯片上集成訪問速度更快的緩存,減小內存訪問的延遲等等。在減小stalls的路上,CPU還採用了不少聰明的技術,好比分支預測,指令重排,寄存器重命名等等。性能

 

GPU則採用了另外一種不一樣的策略:throughput。它提供了大量的專用處理器,因爲GPU端數據的自然並行性,因此經過數據的大規模並行化處理,來下降延遲。這種設計優勢是經過提升吞吐量,數據的總體處理時間減小,隱藏了處理的延遲。可是因爲芯片上集成的核越多,留給其餘設備的空間就越小,因此像memory cache和logical control這樣的設備就會變少,致使每一路shader program的執行變得延遲很高。瞭解這個特性,咱們來看一個例子,以此來講明如何利用GPU的架構,寫出更高效的代碼。優化

 

假如咱們有一個mesh要被渲染,光柵化後生成了2000個fragment,那麼咱們須要調用一個pixel shader program 2000次,假如咱們的GPU只有一個shader core(世界最弱雞GPU),它開始執行第一個像素的shader program,執行一些算數指令,操做一下寄存器上的值,因爲寄存器是本地的,因此此時並不會發生阻塞,可是當程序執行到某個紋理採樣的操做時,因爲紋理數據並不在程序的本地寄存器中,因此須要進行內存的讀取操做,該操做可能要耗費幾百甚至幾千個時鐘週期,因此會阻塞住當前處理器,等待讀取的結果。若是真的只是這樣設計這個GPU,那它真的就是太弱雞了,因此爲了讓它稍微好點,咱們須要提高它的性能,那如何下降它的延遲呢?咱們給每一個fragment提供一些本地存儲和寄存器,用來保存該fragment的一些執行狀態,這樣咱們就能夠在當前fragment等待紋理數據時,切換到另外一個fragment,開始執行它的shader program,當它遇到內存讀取操做阻塞時,會再次切換,以此類推,直到2000個shader program都執行到這裏。這時第一個fragment的顏色已經返回,能夠繼續往下執行了。使用這種方式,能夠最大化的提升GPU的效率,雖然在單個像素來看,執行的延遲變高了,可是從2000個像素總體來講,執行的延遲減小了。編碼

 

現代GPU固然不會弱雞到只有一個shader core,可是它們也一樣採用了這種方式來減低延遲。現代GPU爲了提升數據的並行化,使用了SIMT(Single Instruction Multi Thread,SIMD的更高級版本),執行shader program的最小單位是thread,執行相同program的threads打包成組,NVIDIA稱之爲warp,AMD稱之爲wavefront。一個warp/wavefront在特定數量的GPU shader core上調度執行,warps調度器調度的基本單元就是warp/wavefront。spa

 

假如咱們有2000個fragment須要執行shader program,以NVIDIA爲例,它的GPU包含32個thread,因此要執行這些任務須要2000/32 = 62.5個warps,也就是說要分配63個warps,有一個只使用一半。線程

一個warp的執行過程跟單個GPU shader core的執行過程是相似的,32個像素的shader program對應的thread,會在32個GPU shader core上同時以lock-step的方式執行,當着色器程序遇到內存讀取操做時,好比訪問紋理(很是耗時),由於32個threads執行的是相同的程序,因此它們會同時遇到該操做,內存讀取意味着該線程組將會阻塞(stall),所有等待內存讀取的結果。爲了下降延遲,GPU的warp調度器會將當前阻塞的warp換出,用另外一組包含32個線程的warp來代替執行。換出操做跟單核的任務調度同樣的快,由於在換入換出時,每一個線程的數據都沒有被觸碰,每一個線程都有它本身的寄存器,每一個warp都負責記錄它執行到了哪條指令。換入一個新的warp,不過是將GPU 的shader cores切換到另外一組線程上繼續執行,除此以外沒有其餘額外的開銷。該過程以下圖所示:設計

 

在咱們這個簡單的例子中,內存讀取的延遲(latency)會致使warp被換出,在實際的應用中,可能更小的延遲就會致使warp的換出操做,由於換入換出的操做開銷很是低。warp-swapping的策略是GPU隱藏延遲(latency)的主要方式。可是有幾個關鍵因素,會影響到該策略的性能,好比說,若是咱們只有不多的threads,也就是隻能建立不多的warp,會使隱藏延遲出現問題。3d

 

shader program的結構是影響性能的主要角色,其中最大的一個影響因素就是每一個thread須要的寄存器的數量。在上面例子的講解過程當中,咱們一直假設例子中的2000個thread都是同時駐留在GPU中的。可是實際上,每一個thread綁定的shader program中須要的寄存器越多,產生的threads就越少(由於寄存器的數量是固定的),可以駐留在GPU中的warp就越少。warps的短缺也就意味着沒法使用warp-swapping的策略減緩延遲。warps在GPU中存在的數量稱之爲佔用率,高佔用率意味着有更多的warps能夠用來執行,低佔用率則會嚴重影響GPU的並行效率。

 

另外一個影響GPU性能的因素就是動態分支(dynamic branching),主要是由if和循環引進的。由於一個warp中的全部線程在執行到if語句時,就會出現分裂,若是你們都是執行的相同的分支,那也沒什麼,但但凡是有一個線程執行另外一個分支,那麼整個warp會把兩個分支都執行一遍,而後每一個線程扔掉它們各自不須要的結果,這種現象稱爲thread divergence

 

瞭解了上面的基本原理,咱們能夠看出,整個GPU的設計其實也是一種trade-off,用單路數據的高延遲,來換總體數據的吞吐量,以此來最大化GPU的性能,下降stall。在實際的編碼過程當中,尤爲是shader的編寫過程當中,也要嚴肅影響GPU優化策略的幾個因素,只有這樣,才能寫出更加高效的代碼,真正發揮出GPU的潛力。

 

下一篇會更加詳細的介紹GPU的結構和邏輯管線,若是錯誤,歡迎指正。

相關文章
相關標籤/搜索