當前位置: 妍妍網 > 碼農

GPU架構和渲染技術

2024-02-27碼農

渲染技術總是伴隨著顯卡硬體的升級而發展的,從最初的GeForce 256開始支持T&L,到RTX支持光追,硬體和渲染技術都在不斷更新。作為軟體技術開發人員,平時更多是從軟體視角去理解渲染。為了更進一步了解渲染的本質,本文換一個視角,收集並整理了一些資料,從GPU架構的角度來重新了解一下渲染。

GPU技術篇

GPU架構

GPU概括來講,就是由視訊記憶體和許多計算單元組成。

視訊記憶體( Global Memory )主要指的是在GPU主機板上的DRAM,類似於CPU的記憶體,特點是容量大但是速度慢,CPU和GPU都可以存取。

計算單元通常是指SM(Stream Multiprocessor,流多處理器),這些SM在不同的顯卡上組織方式還不太一樣。作為執行計算的單元,其內部還有自己的控制模組、寄存器、緩存、指令流水線等部件。

計算單元

下面是Maxwell架構圖和Turing架構圖。

Maxwell架構圖

Turing架構圖 從 Fermi 開始NVIDIA使用類似的原理架構。

GPU包含若幹個 GPC(Graphics Processing Cluster,圖形處理簇) ,不同架構的GPU包含的GPC數量不一樣。例如Maxwell由4個GPC組成;Turing由6個GPC組成。

GPC包含若幹個 SM(Stream Multiprocessor,流多處理器) ,不同架構的GPU的GPC包含的SM數量不一樣。例如Maxwell的一個GPC有4個SM;而Turing的一個GPC包含了6個 TPC(Texture/Processor Cluster,紋理處理簇) ,每個TPC又包含了2個SM。

補充 :GPC裏除了有SM還有一些其它的部件,比如 光柵化引擎(Raster Engine) 。另外,連線每個GPC靠的是 Crossbar ,例如某一個GPC計算完的數據需要另外GPC來處理,這個分配就是靠的Crossbar。

這裏的SM就是本章節所說的計算單元,同時需要知道的是,程式設計師平時寫的Shader就是在SM上進行處理的。

SM(Stream Multiprocessor,流多處理器)

不同GPU廠商的架構中,SM的叫法不盡相同。

  • • 高通稱作Streaming Processor / Shder Processor。

  • • Mali稱作Shader Core。

  • • PowerVR稱作Unified Shading Cluster,通常簡稱為Shading Cluster或 USC

  • • ATI/OpenCL稱作Compute Unit,通常簡稱為CU。

  • 下圖展示了一個SM的內部結構。

    以Fermi架構的單個SM來說,其包含以下部件。

  • PolyMorph Engine :多邊形變形引擎。負責處理和多邊形頂點相關的工作,包括以下模組。

  • Vertex Fetch模組 :頂點處理前期的透過三角形索引取出三角形數據。

  • Tesselator模組 :對應著DX11引入的新特性曲面細分。

  • Stream Output模組 :對應著DX10引入的新特性Stream Output。

  • Viewport Transform模組 :對應著頂點的視口變換,三角形會被裁剪準備柵格化。

  • Attribute Setup模組 :負責頂點的插值運算並輸出給後續像素處理階段使用。

  • Core :運算核心,也叫流處理器(SP——Stream Processor)。每個SM由32個運算核心組成。由 Warp Scheduler 排程,接收 Dispatch Units 的指令並執行,下面會詳細介紹。

  • Warp Schedulers :Warp排程模組。Warp的概念其實就是一組執行緒,通常由32個執行緒組成,對應著32個運算核心。Warp排程器的指令透過 Dispatch Units 送到 運算核心(Core) 執行。

  • Instruction Cache :指令緩存。存放將要執行的指令,透過 Dispatch Units 填裝到每個 運算核心(Core) 進行運算。

  • SFU :特殊函式單元(Special function units)。與Adreno GPU中的初等函式單元(Elementary Function Unit,EFU)類似,執行特殊數學運算。由於其數量少,在高級數學函式使用較多時有明顯瓶頸。特殊函式例如以下幾類。

  • • 冪函式:pow(x, a)、sqrt(x)。

  • • 對數函式:log(x)、log2(x)。

  • • 三角函式:sin(x)、cos(x)、tan(x)。

  • • 反三角函式:asin(x)、acos(x)、atan(x)。

  • LD/ST :載入/儲存模組(Load/Store)。輔助一個Warp(執行緒組)從 Share Memory 或視訊記憶體載入(Load)或儲存(Store)數據。

  • Register File :寄存器堆。存放將要處理的數據。

  • L1 Cache :L1緩存。不同GPU架構不一樣,有些L1緩存和 Shared Memory 共用,有的L1緩存和 Texture Cache 共用。

  • Uniform Cache :全域統一記憶體緩存。

  • Tex Unit和Texture Cache :紋理讀取單元和紋理緩存。Fermi有4個 Texture Units ,每個 Texture Unit 在一個運算周期最多可取4個采樣器,這時剛好餵給一個 執行緒束(Warp) (的16個車道),每個 Texture Uint 有16K的 Texture Cache ,並且在往下有 L2 Cache 的支持。

  • Interconnect Network :內部連結網路。

  • GPU記憶體架構

  • GPU類似於CPU也有自己的寄存器、L1 Cache、L2 Cache、視訊記憶體,甚至必要時候還可以使用系統記憶體。

    圖中越往上,存取速度越快,越往下存取速度越慢。其中, Global Memory(全域記憶體) 即我們通常所說的視訊記憶體,通常放在GPU芯片的外部。 L2 Cache 是GPU芯片內部跨GPC而存在的。 L1 Cache/Shared Memory Uniform Cache Tex Unit Texture Cache 以及 寄存器 都是存在於SM內部的。

    它們的存取速度從寄存器到全域記憶體依次變慢:

    儲存型別

    存取周期

    寄存器

    1

    共享記憶體

    1~32

    L1緩存

    1~32

    L2緩存

    32~64

    紋理、常量緩存

    400~600

    全域記憶體

    400~600

    GPU架構和渲染管線

    如果從GPU硬體的視角下來看渲染管線,會看到一些不一樣的東西,下圖為GPU硬體視角下的渲染管線概要。

    套用階段

    這個階段主要是CPU在準備數據,包括圖後設資料、渲染狀態等,並將數據傳給GPU的過程。如下圖所示就是數據如何進入GPU處理的過程。

    CPU和GPU之間的數據傳輸是一個異步的過程,類似於伺服器和客戶端之間的數據傳輸。CPU和GPU構造了一種生產者/消費者異步處理模型。CPU生產「命令」,GPU消費「命令」,透過這種關系CPU就可以將數據和行為傳輸到GPU,GPU來執行對應動作。

    CPU端透過呼叫 渲染API(Graphics API) ,比如DX或者GL,將操作封裝為一個一個的 命令 存放到 命令佇列中(FIFO Push Buffer) ,即上圖中的 PushBuffer

    當記憶體寫滿或者顯示呼叫(Present或者Flush等)送出命令佇列的時候,CPU將命令佇列送出給 套用驅動 ,並在命令佇列末尾壓入一條改變 Fence 值的命令。

    接下來,透過 系統驅動 的排程,輪到這個套用傳輸的時候,就將數據寫入到記憶體中的 RingBuffer 中。RingBuffer好比一個旋轉的水車,將命令一點一點「搬運」到GPU的 前端(Front End) 。當這個「水車」滿了,也就是RingBuffer滿了,CPU就會發生擁塞。

    當命令佇列最後一條命令,也就是修改Fence值的命令被前端接收後,CPU接到了Fence修改的訊號,擁塞就會被解除,CPU繼續執行產生接下來的命令。

    圖元裝配器(Primitive Distributor) 根據圖元型別、頂點索引以及圖元裝配命令,開始分配渲染工作,並行送給多個GPC處理。

    頂點處理

    PolyMorph Engine Vertex Fetch模組 透過三角形索引,將數據從視訊記憶體中取得三角形數據,傳入SM寄存器中。

    前文說過Shader就是在SM上進行處理的。熟悉Shader開發的人都知道,Shader會對不同的「語意」進行處理,這些語意也叫「寄存器」。Shader中使用到的 寄存器 不光這些「語意」的寄存器,它們分為很多種型別,包括輸入寄存器、常量寄存器、臨時寄存器等。

    Shader Model 2.0/2.X

    Shader Model 3.0

    Shader Model 4.0

    臨時寄存器

    ≥12

    32

    4096

    VS常量寄存器

    ≥256

    ≥256

    14×4096

    PS常量寄存器

    32

    224

    14×4096

    VS紋理

    None

    4

    128×512

    PS紋理

    16

    16

    128×512

    VS輸入寄存器

    16

    16

    16

    插值寄存器

    8

    10

    16/32

    PS輸出寄存器

    4

    4

    8

    數據進入SM後, 執行緒排程器(Warp Scheduler) 為每個Shader核心函式(VS/GS/PS等)建立一個執行緒,並在一個 運算核心(Core) 上執行該執行緒。根據Shader需要的寄存器數量,在 寄存器堆(Register File) 中為每個執行緒分配指定數量的寄存器。

    同時, 指令排程單元(Instruction Dispatch Unit) 將Shader中的操作指令從 指令緩存(Instruction Cache) 中取出,並分配給每個 運算核心 去執行。

    對於Shader的這種處理機制,不管是VS(Vertex Shader)還是PS(Pixel Shader),以及GS(Geometry Shader)等著色器來說都是相同的。換句話說,就是無論VS、PS、GS,都是在SM的 運算核心 裏執行每一條指令的。

    那麽要深入理解SM工作的這種機制,這裏需要解釋一下三個重要的概念: 統一著色器架構 SIMT 執行緒束

    統一著色器架構

    Shader Model 在誕生之初就為我們提供了Pixel Shader(頂點著色器)和Vertex Shader(像素著色器)兩種具體的硬體邏輯,它們是互相分置彼此不幹涉的。

    但是在長期的開發過程中,發現了以下的問題。

  • • 如果一個場景包含的三角形相當細碎,那麽這個為了渲染這個場景,頂點著色器的處理單元就會負載很高,但是會有很大一部份像素著色器的處理單元閑置。

  • • 如果一個場景僅包含一個大的三角形,而且這個大三角形覆蓋了大部份的螢幕像素且運算很復雜,那麽像素著色器的處理單元就會負載很高,但是會有一大部份頂點著色器的處理單元閑置。

  • 下圖展示了Vertex Shader和Pixel Shader的負載對比。

    在長期的發展過程中,NVIDIA和ATI的工程師都認為,要達到最佳的效能和電力使用效率,還是必須使用統一著色器架構才能解決上述問題。

    在統一著色器架構的GPU中,Vertex Shader和Pixel Shader概念都將被廢除,取而代之的就是「 運算核心(Core) 」。 運算核心 是個完整的圖形處理體系,它既能夠執行對頂點操作的指令(代替VS),又能夠執行物件素操作的指令(代替PS)。GPU內部的 運算核心 甚至能夠根據需要隨意切換呼叫,從而極大的提升遊戲的表現。

    SIMT

    前文提到 指令(Instruction) 會經過 排程單元(Dispatch Unit) 的排程,分配到每一個運算核心去執行。

    那麽, 指令 是什麽呢?其實指令可以理解為一條一條的操作命令,也就是告訴運算核心要怎麽做的「描述語句」。比如 「 將tmp25號寄存器裏的值加上tmp26號寄存器裏的值,得到的值存入tmp27號寄存器 」這種操作,就是一條指令。

    排程單元這裏分配給每一個執行核心去執行的 指令 其實都是相同的。也就是說 排程單元(Dispatch Unit) 讓每個運算核心在同一刻幹的事情都是一樣的。每一個運算核心雖然同一時刻做的操作是一樣的,但是它們所操作的數據各自都是不同的。

    舉個例子,還是上面的這條指令—— 「將tmp25號寄存器裏的值加上tmp26號寄存器裏的值,得到的值存入tmp27號寄存器」 。對於A運算核心和B運算核心來說,它們各自的tmp25號、tmp26號寄存器裏存的值都是不一樣的,以下為兩個核心可能出現情況的例子。

  • • 對於A運算核心來說,tmp25號存了「 2 」,tmp26號存了「 3 」,最終計算後寫入tmp27號寄存器的數是「 5 」。

  • • 對於B運算核心來說,tmp25號存了「 8 」,tmp26號存了「 12 」,最終計算後寫入tmp27號寄存器的數是「 20 」。

  • 這就是SIMT(Single Instruction Multiple Threads),T(執行緒,Threads)對應的就是運算核心(下文會介紹),轉譯過來叫做「單指令多執行緒(運算核心)」,顧名思義,指令是相同的但是執行緒卻不同。

    透過上文的解釋,我們還了解到了運算核心執行指令的另一個特征:運算核心執行指令的方式叫做「 lock-step 」。也就是所有運算核心同一時間執行的指令都是相同的,只有所有核心執行完當前指令, 排程單元(Dispatch Unit) 才會分配下一條指令給所有運算核心執行。

    執行緒束

    每個SM包含了很多寄存器,每個Shader核心函式(VS/GS/PS等)會當作一個執行緒去執行。Shader經過編譯後,可以明確知道要執行的核心函式需要多少個寄存器,也就是說每個執行緒需要多少個寄存器是明確的。當執行緒要執行時,會從 寄存器堆 上分配得到這個執行緒需要數目的寄存器。比如一個SM總共有32768個寄存器,如果一個執行緒需要256個寄存器,那麽這個SM上總共可以執行32768/256=128個執行緒。

    SM上每一個 運算核心 同一時間內執行一個 執行緒 ,也就是說一個執行緒其實是對應一個 運算核心 ,但是,一個 運算核心 卻是對應多個執行緒。這該怎麽理解呢?

    上文說到Shader所需要的寄存器數量決定了SM上總共能執行多少個執行緒。一個SM上總共也就有32個運算核心,但是如果多於32個執行緒需要執行怎麽辦?

    執行緒排程器 會將所有執行緒分為若幹個組,每一個組叫做一個 執行緒束(Warp) ,它又包含了32個執行緒。因此如果一個SM總共有32768個寄存器,這個SM總共可以執行128執行緒,那麽這個SM上總共可以分配128/32=4個執行緒束。

    一個運算核心同一時間只能處理一個執行緒,一組(32個)運算核心同一時間只能處理一個執行緒束,而執行緒束中有些指令處理起來會比較費時間,特別是記憶體載入。每當當前 執行緒束(Warp) 遇到費時操作,它就會被 阻塞(Stall) 。為了降低延遲,GPU的 執行緒排程器 會采用一種簡單而有效的策略,就是切換另一組執行緒來執行。

    運算核心 在多個 執行緒束(Warp) 間切換著執行,最大化利用運算資源,也就解釋了上文中所描述的執行緒和運算核心之間的關系了。

    下圖展示了一個SM執行三個執行緒束的例子。例子中一個執行緒束只有4個執行緒是一種簡化圖形的表示方式,根據上文可知,其實一個執行緒束中的執行緒數遠大於4。下圖中的txr指令延遲會比較高,所以容易使執行緒阻塞(Stall)。

    細心的你如果仔細思考,也許會產生一個疑問:為什麽 執行緒排程器 不去排程執行緒而是排程執行緒束?

    透過上一小節介紹「 SIMT 」的時候我們了解到運算核心是以 lock-step 的方式執行的,也就是說執行緒執行的「步調」是一致的,每條指令對於所有執行緒來說都是「一起開始一起結束」。所以 執行緒排程器 排程的單位是 執行緒束(Warp)

    由於執行緒束的機制,可以推出以下結論。由於 寄存器堆 的寄存器數量是固定的,如果一個Shader需要的寄存器數量越多,也就是每個執行緒分配到的寄存數量越多,那麽執行緒束數量就越少。執行緒束少,供 執行緒排程器 排程的資源就少,當遇到耗時指令時,由於沒有更多執行緒束去靈活調配,所有執行緒就只能死等,不利於資源的充分利用,最終導致執行效率低下。

    裁剪空間

    當Warp完成了VS的所有指令運算,就會被 PolyMorph Engine Viewport Transform模組 處理,並將三角形數據存放到 L1和L2緩存 裏面。此時的三角形會被變換到裁剪空間(Clip空間),在這個空間下的頂點為像素處理階段做好了準備。

    像素處理

    為了平衡光柵化的負載壓力, WDC(Work Distribution Crossbar) 會根據一定策略,將螢幕劃分成多個區域塊,並重新分配給每一個GPC。下圖為WDC為螢幕劃分區域塊的示意圖。

    前文提到GPC裏有一個 光柵化引擎(Raster Engine) ,這裏GPC接收到分配的任務後,就是交給 光柵化引擎 來負責這些三角形像素資訊的生成。同時還會處理其他的一些渲染流水線步驟,包括 裁剪(Cliping) 背面剔除 以及 Early-Z

    接下來 光柵化引擎 將插值好的數據轉交給 PolyMorph Engine Attribute Setup模組 ,將Vertex Shader計算後存放在 L1和L2緩存 裏面的數據載入出來,經過插值的數據填充到Pixel Shader的 寄存器 裏,供SM的 運算核心 做像素計算的時候使用。

    在邏輯上,一個執行緒執行一個Pixel Shader的核心函式,也就是一個執行緒處理一個像素。GPU將螢幕分成一個一個的2×2的 像素塊 ,因為邏輯上一個Warp包含了32個執行緒,也就是說一個Warp處理的是8個像素塊。

    上文提到WDC會根據一定策略劃分區域塊,實際上的劃分可能比上圖更加復雜。網上有博主根據 NV shader thread group [1] 提供的OpenGL擴充套件,基於OpenGL 4.3+和Geforce RTX 2060做了如下實驗。

    首先,應用程式畫了兩個覆蓋全螢幕的三角形。頂點著色器就不贅述了,下面看看片元著色器。

    圖中有32個亮度色階也就說明有32個不同編號的SM,由渲染結果可以看到SM的劃分並不是按編號順序簡單地依次劃分的。另外根據上圖可見,同一個色塊內的像素分屬不同三角形,就會分給不同的SM進行處理。如果三角形越細碎,分配SM的次數就會越多。

    這裏一個色塊是16×16,也就說明一個SM裏執行了256個執行緒。

    將片元著色器改為如下程式碼,顯示Warp的分布情況。

    // warp id
    float lightness = gl\_WarpIDNV / gl\_WarpsPerSMNV;
    FragColor = vec4(lightness);

    渲染畫面如下圖所示。

    由於一個色塊是由4×8個像素組成,也就證明了一個Warp包含了32個執行緒。

    輸出到渲染目標

    經過PS計算,SM將數據轉交給 Crossbar ,讓它分配給 ROP(渲染輸出單元) 。像素在這裏進行 深度測試 以及 幀緩沖混合 等處理,並將最終值寫入到一塊 FrameBuffer 裏面,這塊FB就是雙緩沖技術裏面的 後備緩沖 。最終將FB寫入到 視訊記憶體(DRAM) 裏。

    多平台渲染架構

    關於IMR、TBR、TBDR介紹的文章有很多,下面簡單歸納一下。

    IMR

    IMR架構主要是PC上GPU采用的渲染架構,這個架構主要是渲染快、頻寬消耗大。

    特點:

  • • 每一個Drawcall按順序、連續地執行完成。每一個Drawcall從VS、PS到最終寫入FrameBuffer中的顏色緩沖、深度緩沖,中間沒有打斷。

  • • FrameBuffer可以被多次存取。也就是說每個Drawcall的每像素渲染都會直接寫入FrameBuffer。

  • • 每個像素頻繁存取視訊記憶體上的FrameBuffer,頻寬消耗大。

  • IMR模式的GPU執行的虛擬碼如下。

    for (draw in renderPass)
    {
    for (primitive in draw)
    {
    execute\_vertex\_shader(vertex);
    }
    if (primitive is culled)
    break;
    for (fragment in primitive)
    {
    execute\_fragment\_shader(fragment);
    }
    }

    問題:

  • • 發熱量大。主要是頻寬消耗大導致的,這個在PC上沒有太大問題。

  • • 耗電量大。主要是頻寬消耗大導致的,這個在PC上沒有太大問題。

  • • 芯片大小大。這個在PC上沒有太大問題,為了最佳化頻寬會有L1 Cache和L2 Cache,所以芯片會變大。

  • TBR

    TBR全稱Tile-Based Rendering,是一種基於分塊的渲染架構。

    分析:

  • • 發熱、費電,行動裝置接受不了。

  • • 芯片太大,行動裝置接受不了。

  • 為了解決以上IMR的問題,行動裝置上的芯片采用了不一樣的設計思路:不直接往視訊記憶體的FrameBuffer裏寫入數據,而是將螢幕分成小塊(Tile),每一個小塊數據都存在On Chip Memory(類似L1和L2的緩存)上,合適的時候一次性渲染並將所有分塊數據從On Chip Memory寫入視訊記憶體的FrameBuffer裏。

    由於不會頻繁寫入FrameBuffer,頻寬消耗降低了,發熱、耗電量問題都解決了;由於分塊寫入快取On Chip Memory,芯片大小問題解決了。

    特點:

  • • 每一個Drawcall執行時僅僅經過分塊(Tiling)和頂點計算,存入FrameData。「合適」的時機(如Flush、clear)進行Early-Z、著色、各種測試,最終一次性寫入FrameBuffer中的顏色緩沖、深度緩沖,中間過程是不連續。

  • • FrameBuffer存取次數很少,FrameData會被頻繁存取。

  • • 由於分塊(Tile)的顏色緩沖和深度緩沖會放到On Chip Memory上,Early-Z和Z-Test都在這上面進行,節省頻寬。

  • TBDR

    TBDR全稱Tile-Base-Deffered Rendering,是一種基於分塊的延遲渲染架構。

    分析:

  • • Early-Z可以很好的降低Overdraw,但是TBR依賴物體繪制順序。如果物體迴圈遮擋,無法完美地做到降低Overdraw。

  • PowerVR設計了一個叫做 ISP(Image Signal Processor) 的處理單元,不依賴物體從遠到近繪制,而是對圖形做像素級的排序,完美透過像素級Early-Z降低Overdraw,這種技術稱為 HSR(Hidden Surface Removal)

    類似的技術比如: Adreno Early Z Rejection Mali 的**FPK(Forward Pixel Killing)**。

    相關渲染最佳化


    寄存器充分使用

    前文提到過,如果寄存器使用過多,會導致Warp數量變少,使得GPU遇到耗時操作的時候沒有空閑Warp去排程,不利於GPU的充分利用,因此要 節約使用寄存器

    對於Shader的語意也好,寄存器也好,都是作為向量存在的。對於GPU的ALU來說,一條指令可以處理的數據一般是四維(4D)的,這就是 SIMD (Single Instruction Multiple Data),類似的SIMD指令可以參考SSE指令集。

    例如下面的程式碼。

    float4 c = a + b;

    如果沒有SIMD處理單元,組譯虛擬碼如下,四個數據需要四個指令周期才能完成。

    ADD c.x, a.x, b.x
    ADD c.y, a.y, b.y
    ADD c.z, a.z, b.z
    ADD c.w, a.w, b.w

    而使用SIMD處理後就變為一條指令處理四個數據了,大大提高了處理效率。

    SIMD_ADD c, a, b

    由於SIMD的特性,寄存器要盡可能完全利用。例如Unity裏有一個宏用來縮放並且偏移圖片采樣用的UV座標—— TRANSFORM_TEX 。按道理縮放UV需要乘以一個二維向量,偏移UV也需要加一個二維向量,這裏應該是需要兩個寄存器的。然而Unity將兩個二維向量都裝入同一個四維向量裏面(xy為縮放,zw為偏移),這樣就只用到一個寄存器了。總而言之, 要充分利用寄存器向量的每一個分量 。其定義如下。

    // Transforms 2D UV by scale/bias property
    #define TRANSFORM\_TEX(tex,name) (tex.xy \* name##\_ST.xy + name##\_ST.zw)

    為了充分利用SIMD運算單元,GPU還提供了一種叫做 co-issue 的技術來最佳化程式碼。例如下圖,由於float數量的不同,ALU利用率從100%依次下降為75%、50%、25%。

    為了解決著色器在低維向量的利用率低的問題,可以透過合並1D與3D或2D與2D的指令。例如下圖,原本的兩條指令,co-issue會自動將它們合並,這樣一個指令周期就可以執行完成。

    但是如果其中一個變量既是運算元,又是儲存數,則無法啟用co-issue來最佳化。

    於是 純量指令著色器(Scalar Instruction Shader) 應運而生,它可以有效地組合任何向量,開啟co-issue技術,充分發揮SIMD的優勢。

    邏輯控制語句

    GPU和CPU由於其設計目標就有很大的區別,於是出現了非常不同的架構。

    CPU有大量的儲存單元(紅色部份)和控制單元(黃色部份),相比起GPU來說,CPU的計算單元(綠色部份)只占了很少一部份。因此CPU不擅長大規模平行計算,更擅長於邏輯控制。相反,GPU擅長大規模平行計算,不擅長邏輯控制。

    因此,不要在Shader裏寫邏輯控制語句,包括if-else和for迴圈等邏輯。下面介紹一下兩種芯片在分支控制上都有哪些區別。

    CPU - 分支預測

    有人在JVM上做過一個測試。如果有一個有序陣列,和一個同樣大的無序陣列,分別取出一百萬次其陣列中大於128的數位之和,消耗的時間是否相同呢?

    long long sum = 0;
    for (unsigned i = 0; i < 1000000; ++i)
    {
    for (unsigned c = 0; c < arraySize; ++c)
    {
    if (data[c] >= 128)
    sum += data[c];
    }
    }

    以上這段程式碼,按實驗步驟來做。

  • • 第一次data陣列是個無序陣列,消耗時間為18.7739秒,sum = 312426300000。

  • • 第二次data陣列事先給排好序,消耗時間是5.69566秒,sum = 312426300000。

  • 兩次實驗數據數量、迴圈次數以及實驗算出來的最終結果都是一樣的,為何兩次消耗時間相差竟有3倍之多?要了解這個問題,就需要了解CPU的分支預測了。

    一條指令在CPU上執行,需要經過以下四個步驟:

    1. 1. fetch(獲取指令)

    2. 2. decode(解碼指令)

    3. 3. execute(執行指令)

    4. 4. write-back(寫回數據)

    比較笨一點的辦法就是,每一條指令等上一條的這四步都走完再執行。顯然這樣效率不是很高,其實當一條指令開始執行第二步 decode 時,下一條指令就可以開始執行第一步 fetch 了。同理,當一條指令開始執行 execute ,下一條指令就可以執行 decode 了,再下一條指令就可以執行 fetch 了。

    那麽如果 if指令 已經執行到 decode 了,接下來該執行if語句塊裏面的指令,還是該執行else語句塊裏面的指令,CPU還不知道,因為只有 if條件判斷 執行完成才能知道接下來該執行哪個語句塊裏的指令。

    此時,CPU會先嘗試將上一次判斷的歷史記錄拿來這一次作為判斷條件來先用著,這就是所謂的「 分支預測 」。如果預測對了,那麽對於效能來說就是賺了;如果預測錯誤,那麽就從 fetch 開始用正確的值重新來執行,也不虧效能。

    GPU - 遮掩

    GPU講究的是大規模平行計算,沒有那麽強大的邏輯控制,所以GPU也不會去做分支預測。那麽GPU是怎麽去處理條件分支判斷的呢?

    由於GPU的執行是以 lock-step 的方式鎖步執行的,也就是每一個 運算核心 一定要執行完當前指令的所有步驟才能執行下一條指令,也就是前文中所說的「比較笨一點的辦法」,所以GPU是沒有分支預測的。但是GPU有一個特殊的機制叫做 遮掩(mask out)

    如上圖所示,同時有8個執行緒在執行右邊程式碼的指令。有的執行緒滿足x大於0,那麽左圖中黃色部份就會被執行,但是同一指令周期內,其他的執行緒x小於或等於0,這些執行緒的指令就會被 遮掩 ,也就是雖然也是要消耗時間,但是不會被執行,就處於了等待。同理,滿足else條件的語句在接下來的指令執行周期內執行淺藍色部份,不滿足的在同一指令周期內就被遮掩,處於等待。

    被遮掩的程式碼同樣是要消耗相同的指令周期時間去等待未被遮掩的程式碼執行。因此,如果一個Shader裏有太多的if-else語句,就會白白浪費很多時間周期。

    同樣的原理套用在for迴圈上,如果有的執行緒迴圈3遍,有的執行緒迴圈5遍,就需要等待迴圈最久的那個執行緒執行完成才能繼續往下執行,造成很多執行緒的浪費。

    因此,在Shader中不是不能寫邏輯控制語句,而是要思考一下有沒有被浪費的資源。換句話說, Shader裏不要用不固定的數值來控制邏輯執行

    減少呼叫費時指令

    通常一些需要從緩存裏,甚至記憶體裏讀取數據的操作會比較費時,例如貼圖采樣的指令。

    從上文中可以了解到,一般GPU架構裏 SFU 這種處理單元比較少,因此特殊數學函式盡量少呼叫,例如pow、sin、cos等。

    移動渲染架構的最佳化

    及時clear或者discard

    由於TB(D)R架構下數據會一直積攢到FrameData裏,直到「合適」的時機才會清空。如果一直不呼叫clear指令就會一直將數據積累到FrameData裏清不掉。如果不用RenderTexture了就及時Discard掉。

    例如有一張RenderTexture,渲染之前呼叫clear就能清空前一次的FrameData,不用這張RenderTexture了,就及時呼叫Discard(),以提高效能。

    不要頻繁切換RenderTexture

    頻繁切換RenderTexture會導致頻繁將Tile數據拷貝到FrameBuffer上,增加效能消耗。

    Early-Z

    Early-Z可以很好的降低Overdraw,但是某些操作會使Early-Z失效。

  • • Alpha Test / Clip / discard:需要執行完 PS 後,才能確定該像素深度是否被寫入。

  • • 手動修改GPU插值得到的深度。

  • • 開啟透明混合( AlphaBlend )。

  • • 關閉深度測試。

  • 特別說明 :因為Early-Z是透過深度去遮蔽不透明物體的,如果透明物體(Alpha Blend)或者挖洞的物體(Alpha Test)透過深度測試遮蔽了背景的不透明(Opaque)物體,那麽背景就會鏤空,看到clear指令指定的固有色,就會出現渲染錯誤,因此無論IMR還是T(D)BR的Early-Z都會受Alpha Test影響。

    因此要做到以下幾點。

  • • 渲染物體時,渲染程式要按「Opaque → AlphaTest → AlphaBlend」的順序渲染物體。

  • • 由於一般來說地形覆蓋面積最大,「Opaque」的內部可以按「其他不透明物件 → 地形」的順序渲染,最大化利用Early-Z最佳化Overdraw。

  • • 無論PowerVR還是Mali/Adreno芯片,AlphaTest都會影響效能,盡量少使用AlphaTest技術。

  • • 不支持Early-Z的硬體,可以適當使用PreDepathPass多渲染一遍圖元來最佳化Overdraw,但是會增加頂點繪制的負擔,需要權衡。

  • 避免大量drawcall和頂點數

    FrameData裏會儲存當前幀變換過的圖元列表,也就是頂點數據,FrameData數據會隨著Drawcall數增加而增加,FrameData增大有可能會儲存到其他地方,影響讀寫速度,因此在行動平台渲染上百萬個頂點或者三四百Drawcall就比較吃力了。

    總結

    本文主要歸納了GPU內部的一些基本單元及其作用,簡單總結了一下對渲染架構的描述,並針對以上兩方面介紹了一些最佳化效能的技巧。本文更多是歸納總結性質的,如果要更加深入的了解可以細讀以下參考文章,如果有總結不到位的歡迎探討。

    參考

    【GPU Fundamentals】[2]

    【Life of a triangle - NVIDIA's logical pipeline】[3](轉譯[4])

    【深入GPU硬體架構及執行機制】[5]

    【CPU 的分支預測】[6]

    【移動端GPU——渲染流程】[7]

    【剖析虛幻渲染體系(12)- 移動端專題Part 2(GPU架構和機制)】[8]

    【針對移動端TBDR架構GPU特性的渲染最佳化】[9]

    【Unity 著色器中"discard"操作符的問題】[10]

    【行動平台GPU硬體學習與理解】[11]

    【圖形 3.4+3.5 正向渲染/延遲渲染 和 深度測試Early-z / Z-prepass(Z-buffer)】[12]

    免責申明: 本號聚焦相關技術分享,內容觀點不代表本號立場, 可追溯內容均註明來源 ,釋出文章若存在版權等問題,請留言聯系刪除,謝謝。

    推薦閱讀

    更多 架構相關技術 知識總結請參考「 架構師全店鋪技術資料打包 (全) 」相關電子書( 41本 技術資料打包匯總詳情 可透過「 閱讀原文 」獲取)。

    全店內容持續更新,現下單「 架構師技術全店資料打包匯總(全) 」一起發送「 和「 pdf及ppt版本 ,後續可享 全店 內容更新「 免費 」贈閱,價格僅收 249 元(原總價 339 元)。

    溫馨提示:

    掃描 二維碼 關註公眾號,點選 閱讀原文 連結 獲取 架構師技術全店資料打包匯總(全) 電子書資料詳情