當我們談優化時,我們談些什麼

遊資網發表於2019-06-12
當我們談優化時,我們談些什麼

前言

過去幾年裡,我經歷過大約幾十場面試,幾乎在每次面試的時候,面試官都會問提一個問題:“你在渲染效能優化方面有什麼經驗?”這個時候我就會開始揣測面試官的意圖,試著去回憶他之前提的問題,看看面試官到底想聽什麼樣的回答:往往這種嘗試最後都是失敗的,結果就是不知道從何說起。因為沒有具體的情境,最後只能說“整個渲染流程中很多地方都可能出現效能瓶頸,只能case by case的去看,找到專案的具體瓶頸,然後針對性地去解決。”幾乎所有聽到這個回答的面試官都會對我意味深長地一笑,不置可否:一旦看到這種笑容,我就知道糟了。之後的面試反饋中,很多人對我的評價就是“對渲染演算法比較熟悉,但是在效能優化方面經驗欠缺”。

總得來說我覺得這不是一個好問題,因為太過寬泛而沒有針對性。我並不想泛泛地說“減少模型數量,減少/合併draw call,縮減貼圖尺寸,壓縮貼圖,使用LOD”,因為這就是所謂“正確但無用的話”:所有遊戲不都是這麼優化麼?此外,對於一個專案來講,模型的面數,貼圖尺寸,LOD的級別這些資訊往往是在DEMO階段就已經由TA主導確立的。對於引擎程式設計師來講,需要你提出優化方案的,通常是在專案的開發過程中產生的新瓶頸(當然你首先需要定位它)。但反過來,我的回答其實也一樣是“正確的廢話”:所有效能優化的流程不都是這樣嗎?

當我們談優化時,我們談些什麼
一個典型的效能優化的流程,從profile開始,到確定瓶頸,然後針對瓶頸優化,測試優化的效果,再進入下一輪的profile(一個效能的優化有可能會導致新的效能瓶頸產生),如此無限迴圈

所以,當我們談論效能優化的時候,我們究竟在談些什麼呢?

我試著理解了這個問題的意圖:如果我們換一種問法,比如“渲染常見的效能瓶頸有哪些?具體可能出現在什麼樣的情景下?為什麼這些情景會造成對應的效能瓶頸?”會不會是一個更好的問題?所以這篇文章,是在試著回答這個新的問題。不同於以往的文章,優化本身確實是一個比較寬泛的主題,所以本文的組織也相對比較鬆散,很多內容可能是我想到哪兒寫到哪兒。其中有些概念基於我對硬體的理解,如有錯誤之處,歡迎指正。

說說GPU的架構

核彈廠有一篇關於自家GPU架構和邏輯管線的非常好的文章[1],如果你想要對GPU的結構有一個比較完整系統的認識,請一定不要錯過這篇Life of a Triangle。比較可惜的是,這篇文章只更新到Maxwell這代架構,沒有較新的Pascal架構(GTX10x0系列)和Turing架構(RTX20x0)的技術細節。不過總體來說,現代GPU的設計架構已經趨於穩定,一般只是針對某些單元做優化,或者增加feature,所以文章中的大部分內容在這裡仍然是有效的,這是文中的一張圖:

當我們談優化時,我們談些什麼

這張圖是基於資料的流向,對GPU的硬體單元進行了大致的劃分,實際上GPU中,最核心的部件可以被分成三大塊,我畫了圖來示意他們大致的協作模式:

當我們談優化時,我們談些什麼

通常來說,GPU會有三個比較重要的部分,分別是控制模組,計算模組(圖中的GPC)和輸出模組(圖中的FBP)。通常來說,GPU架構的設計需要有可伸縮性,這樣通過增加/閹割計算和輸出模組,就能夠產生效能不同的同架構產品(比如GTX1070和GTX1080的主要區別就在於GPC和FBP的數量),以滿足不同消費水平和應用場景的需求。

控制模組

控制模組負責接收和驗證(主要是Host和Front End)來自CPU的經過打包的PushBuffer(經過Driver翻譯的Command Buffer),然後讀取頂點索引(注意是Vertex Indices不是Vertex Attributes,主要由Primitive Distributor負責)分發到下游管線或者讀取Compute Grid的資訊(主要由CWD負責,這部分是Compute Pipeline,不作展開)並向下遊分發CTA。

Tips:計算管線和圖形管線共享大部分的晶片單元,只在分發控制的單元上各自獨享(PD和CWD)。許多較新的Desktop GPU允許圖形和計算管線並行執行,可以在一些SM壓力輕的圖形計算環節(比如Shadow Map繪製),利用Compute Shader去做一些SM壓力重的工作(比如後處理),讓各個硬體單元的負載更加平衡[2]。

計算模組

計算模組是GPU中最核心的部件,Shader的計算就發生在這裡。早期的硬體設計上,我們會區分VS,PS等Shader型別,並設計專用的硬體單元去執行對應型別的Shader,但這樣的方法並不利於計算單元滿負荷運轉,所以現在所有的GPU設計都是通用計算單元,為所有Shader型別服務。在NV的顯示卡里這個模組全稱是Graphics Processing Cluster,通常一個GPU會有多個GPC,每個GPC包含一個光柵器(Raster)負責執行光柵化操作,若干個核心的計算模組,稱之為Texture Process Cluster(TPC),關於TPC,我們進一步分解來看這張大圖[3]:

當我們談優化時,我們談些什麼

通常來說,一個TPC擁有:

(1)若干個用於貼圖取樣的紋理取樣單元(Texture Units)

(2)一個用於接收上游PD資料的Primitive Engine,PE作為一個固定單元,負責根據PD傳來的頂點索引去取相應的頂點屬性(Vertex Attribute Fetch),執行頂點屬性的插值,頂點剔除等操作

(3)一個負責Shader載入的模組

(4)若干執行Shader運算的計算單元,也就是流處理器(Streaming Multi-Processor,SM,AMD叫CU)

TPC內最核心的部件就是SM,這裡我們再進一步分解SM看這張大圖:

當我們談優化時,我們談些什麼

一個SM通常擁有一塊專用於快取Shader指令的L1 Cache,若干執行緒資源排程器,一個暫存器池,一塊可被Compute Pipeline訪問的共享記憶體(Shared Memory),一塊專用於貼圖快取的L1 Cache,若干浮點數運算核心(Core),若干超越函式的計算單元(SFU),若干讀寫單元(Load/Store)。

作為核心計算單元,GPU的設計思路和CPU有很大的不同,就我所知的體現在兩個方面:

(1)GPU擁有較弱的流程控制(Flow-Control)的能力

(2)GPU擁有更大的資料讀寫頻寬,並配合有更多樣的延遲隱藏技術

GPU的執行模型

要詳細解釋這兩點,我們就需要理解GPU的執行模型:GPU的設計是為了滿足大規模並行的計算,為此,它使用的是SIMD(Single Instruction Multiple Data)的執行模式,在內部,若干相同運算的輸入會被打包成一組並行執行,這個組就是GPU的最小執行單元,在NV叫做Warp,每32個thread為一組,在AMD叫做Wavefronts,每64個thread為一組。基於不同的shader階段,被打包執行的物件會有區別,比如VS裡,就是32個頂點為一組,PS裡,就是8個pixel quad(2*2畫素塊)為一組。

那麼GPU又如何處理分支呢?我們知道,CPU有一種經典的處理分支的方法,叫做分支預測[4]。CPU會根據一組資料之前的分支結果去預測下一次分支的走向,如果錯誤就會有額外的開銷。GPU沒有這麼複雜的流程控制,它的流程控制基於一種叫做“active mask”的技術,簡單來說就是用一個bit mask去判斷當前32個thread的branch狀態,如果是0,則表示只需要執行false的branch,如果bit mask是2^32-1,則表示只需要執行true的branch,否則就需要對某些thread執行true,同時另一些在執行true的同時等待這些thread,反之亦然,這種情形下一個warp的執行時間就是Max(branch(true))+Max(branch(false))。

GPU的記憶體型別

Desktop GPU的記憶體型別和CPU比較相似,也是多級快取的機制,我們能夠接觸到的記憶體型別包括Register,Shared Memory(本質是L1 Cache的一塊),Texture L1 Cache(本質是L1 Cache的一塊),Instruction Cache(本質是L1 Cache的一塊),L2 Cache,DRAM,各類儲存器的容量在是依次增大的,相應的它們在晶片上的位置也是離核心單元SM越來越遠,同時訪存延遲也是逐級增大的。

關於Desktop的記憶體型別,包括更多的延遲隱藏技術是一個比較大的話題,這裡無法再詳細展開,可以去參考其他文獻[5]。

對於GPU在這幾種記憶體中的訪存延遲,我從這篇文章[6]找到了一些資料:

當我們談優化時,我們談些什麼
給小白的tips:$表示cache,剛進NV的時候我也不知道這是啥意思

對於PC端儲存器的速度,可以檢視這個網站。

Mobile GPU沒有專用的視訊記憶體,而是和CPU共享同一塊系統記憶體(快取機制當然也應該是共享的),但它有一塊位於GPU上的專用on chip memory,這裡沒有找到Mobile GPU上的延遲資料,如果有相關資料請告訴我。

GPU擁有大量的暫存器(數量遠多於CPU),是為了能夠快速的在warp之間做切換:當某個warp被某些指令阻塞的時候(比如貼圖取樣),warp schedular可以讓其處於休眠狀態,並且把shader core的資源讓出來,喚醒那些未被阻塞的warp。對於CPU來說,context switch的開銷來源於暫存器的恢復和儲存(沒那麼多暫存器,只能複用),但是對於GPU,每個warp是獨佔一份自己的register file的,這樣就可以幾乎無消耗地切換warp。相應的,一個SM裡能同時並行多少個warp,就取決於一段shader到底佔用了多少register,佔用的越多,則能夠並行的warp就越少。

Tips:如果沒有記錯的話,一直到Turing之前的架構,同一個SM內都只能執行一個shader,新的Turing架構似乎是允許SM內執行不同shader。

這裡補一張圖簡單說一下Turing架構它和上幾代顯示卡在SM上的區別:

當我們談優化時,我們談些什麼

相較於上幾代的GPU,Turing在SM中增加了專用於光線追蹤的RTCore,以及用於張量計算的TensorCore(後者主要是用於深度學習。在Turing之後,你還可以在做Graphics的同時利用TensorCore去做一些DL的工作,比如DLSS[7]?好像沒什麼x用)。下面兩張圖簡單解釋了RTCore前後光線追蹤的基本流程:

當我們談優化時,我們談些什麼

當我們談優化時,我們談些什麼

這個圖看起來很複雜,其實很簡單:對於非Turing架構來說,光線和BVH的遍歷求交、光線和三角形的求交、光線和三角形交點的著色這三件事,都是翻譯成了數千條SM的指令給FP Core執行的。而Turing架構則是把前兩件事作為固定硬體單元整合在了RTCore裡,所以RTCore核心功能有兩個:遍歷BVH和光線-三角形快速求交。


輸出模組

輸出模組(Framebuffer Partition,FBP)比較簡單,最核心的部件是一個稱之為ROP(Raster Operation)的單元,ROP又包含了兩個子單元,分別是CROP(Color ROP)和ZROP,前者負責Alpha Blend,MSAA Resolve等操作,並把最終的顏色寫到color buffer上,後者則負責進行Stencil/Z Test以及把depth/stencil寫到z buffer上。

Pascal和Turing架構的補充

相較於Maxwell,Pascal架構在圖形方面的feature主要針對VR渲染(Lens Matched Shading,Single Pass Stereo等),因為當時恰好是VR概念大火的一年,具體的技術細節可以參考這篇文章[8]。而Turing架構在圖形方面最大的feature莫過於引入了實時光線追蹤,針對VR和可程式設計管線部分也有比較進一步的優化,具體可以參見這篇文章[9]。有關光線追蹤的技術,我在之前的兩篇文章[10][11]中有比較詳細的解釋。

在工藝製程和硬體引數方面,這篇文章[12]也給了我們一些參考資料:

當我們談優化時,我們談些什麼

當我們談優化時,我們談些什麼

如果你希望對這些引數包括各類顯示卡的引數包括新特性有更深入的瞭解,也可以去試著讀一讀各代顯示卡的技術白皮書[13][14]。如果這些細節還不足以滿足你對硬體的好奇心,那麼強烈建議你去核彈廠工作。

Mobile GPU和Desktop GPU的差異

目前市面上主流的Mobile GPU生產商(Qualcomm,ARM,Imagination)的GPU架構都是由Desktop GPU發展而來,因此在硬體架構上同桌面級GPU差異不太大,值得一提是這幾點[15]:

(1)Mobile GPU的晶片面積和功率遠小於Desktop GPU,兩張圖:

當我們談優化時,我們談些什麼

當我們談優化時,我們談些什麼

(2)Mobile GPU位於SoC上,和CPU共享記憶體,SoC是整體供電的,沒有專用的電源輸出到GPU

(3)移動裝置是被動式散熱,整個SoC的供電基於Thermal Throttling機制,當裝置功率過高發熱過量時,電源會降低輸送功率防止SoC過熱,這意味著如果CPU負擔過大觸發這一機制,同樣也會使得GPU的效能下降

對於低端Mobile GPU,通常限制效能的是晶片面積,而對高階Mobile GPU,限制效能的則是頻寬和發熱。

回到渲染管線

回到我們一開始說過的那句“無用的廢話”:渲染管線的任何階段都有可能成為效能瓶頸。那麼如果你試圖列舉大部分可能的效能瓶頸,就首先需要對整個GPU的渲染管線比較熟悉,並且能夠大致地知道GPU在渲染管線的每個階段,大致都做了哪些事,我們的每一個Graphics API Call在GPU端又對應著什麼樣的行為?基於這樣的“翻譯”,我們才能夠理解那些效能瓶頸產生的原因,也能夠理解我們之前說到的那些“泛泛”的優化策略到底為什麼能夠解決一些效能上的瓶頸。

說到渲染管線,就必須介紹當前GPU使用的三種不同的渲染管線:Immediate Mode Rendering,Tile Based Rendering和Tile Based Deferred Rendering[16]。我們用三張圖來詳細描述IMR,TBR和TBDR三種模式的渲染管線:

IMR模式的管線

當我們談優化時,我們談些什麼

IMR模式的第一個階段是Vertex Processing,這個環節包括從DRAM/System Memory取Vertex Indices(PD的工作),然後根據Vertex Indices去Vertex Buffer取相應的屬性(PE中VAF的工作),需要注意的是,取Indices/Vertex Attributes的階段都會有L2 Cache在工作,表示如果頂點短時間內被share多次,則可以通過cache命中減少載入時間。載入完頂點資料後,Vertex Shader將會被載入到SM的Instruction Cache,緊接著就是VS在SM的執行。

VS執行完畢後,PE內的固定單元會執行頂點剔除來剔除一些視口外的三角形,背面剔除也在這個階段發生。

接下來,由Raster對三角形進行光柵化,光柵化完畢的畫素將會被打包成warp,經過XBAR重新流入SM(可能是同一個SM,也可能是不同的SM)。重新進入SM的每個pixel會根據其重心座標,使用PE內的固定單元進行屬性插值,從而得到depth,varying attributes等資訊。

對於沒有Apha Test的pixel quad,由ZROP對其執行early-Z test。

對於通過early-Z test的畫素,在SM內執行pixel shader。

對於開啟Alpha Test的畫素,由ZROP對其進行late-Z test,並根據結果決定是否更新FrameBuffer相應位置的顏色和深度值。

若需要更新,則ZROP根據depth test的設定更新z buffer,CROP根據blend的設定去更新color buffer。

注意,IMR的整個流程中,三角形是可以以Stream的形式逐步提交給管線的,先提交的三角形也不需要去等待同一個Render Target上的其他三角形。

有關IMR管線的描述,這篇slides[17]的描述比本文要詳細很多,非常建議仔細閱讀。

IMR是所有Desktop GPU的標配,因為Desktop GPU相較於Mobile GPU,有更多的頻寬用於讀寫,有專用供電介面,也不受限於晶片發熱的問題。IMR架構的好處是設計上會相對來說比較清晰簡明,並且整個管線是連續的,draw call之間不需要互相等待,有利於最大化吞吐量。對於Mobile GPU來說,只有NV的Tegra系列是基於IMR的

TBR模式的管線

當我們談優化時,我們談些什麼

TBR架構的GPU會把整個邏輯渲染管線打斷成兩個階段

第一階段和IMR類似,它負責頂點處理的工作,不同的是在每個三角形執行完他們的VS之後,還會執行一個稱之為Binning Pass[18]的階段,這個階段把framebuffer切分成若干個小塊(Tiles/Bins),根據每個三角形在framebuffer上的空間位置,把它的引用寫到受它影響的那些Tiles裡面,同時由VS計算出來的用於光柵化和屬性插值的資料,則寫入另一個陣列(我們可以認為圖中Primitive List就是我們說的一個固定長度陣列,其長度依賴於framebuffer劃分出的tile的數量,陣列的每個元素可以認為是一個linked list,存的是和當前tile相交的所有三角形的指標,而這個指標指向的資料,就是圖中的Vertex Data,裡面有VS算出的pos和varying變數等資料)。在Bining Pass階段,Primitive List和Vertex Data的資料會被寫回到System Memory裡。

Tips:TBR的管線會等待同一個framebuffer上所有的三角形的第一階段都完成後,才會進入到第二階段,這就表示,你應該儘可能的少切換framebuffer,讓同一個framebuffer的所有三角形全部繪製完畢再去切換

第二階段負責畫素著色,這一階段將會以Tile為單位去執行(而非整個framebuffer),每次Raster會從Primitive List裡面取出一個tile的三角形列表,然後根據列表對當前tile的所有三角形進行光柵化以及頂點屬性的插值。後面的階段TBR和IMR基本是一致的,唯一區別在於,由於Tile是比較小的,因此每個Tile的color buffer/depth buffer是儲存在一個on chip memory上,所以整個著色包括z test的過程,都是發生在on chip memory上,直到整個tile都處理完畢後,最終結果才會被寫回System Memory。

Tips:TBR的優化實際上是利用快取的區域性性原理。

TBDR模式的管線

當我們談優化時,我們談些什麼

TBDR和TBR模式基本類似,唯一的區別在於,TBDR模式在執行光柵化之後,不會急著shading,而是會對rasterized sample進行消隱(基於depth buffer和相同位置的其他sample深度去移除被遮擋的sample),這個消隱的過程結束之後,tile上剩下的sample才會被送到PS裡面去做shading。

Tips:通常TBR/IMR模式的GPU是基於比較簡單的early-Z reject去防止overdraw,TBDR在這個方面則走得更遠一點。所以對於IMR/TBR模式的GPU來說,對不透明物體的draw call從前到後排序、Pre-Z pass都能夠顯著減少overdraw並提高效能;但對於TBDR模式的GPU來說,這兩個策略都不會提升效能(管線裡面做了相同的事),而且還會影響因效能(排序、Pre-Z pass帶來的額外開銷)。

Tips:渲染管線中說的TBDR和我們在引擎的渲染管線中說的TBDR不是一回事,但是這兩者又有很大的關係。

關於這三種模式的區別以及演化,強烈建議配合演示動畫仔細閱讀這篇文章[18]。

什麼情景會造成效能瓶頸?

Imagination有兩篇[19][20]關於自家PowerVR系列顯示卡的效能優化建議,其中列舉了一些常見的效能優化場景。作為本文的Case Study部分,我會在這兩篇的基礎上結合前面硬體的原理,去解釋其中一些建議的原因。

幾何資料優化

減少頂點數量

這個優化簡單又直接,減少頂點意味著更少的頂點從System Memory/DRAM裡讀取到Shader Core(頻寬壓力),同時意味著更少的VS執行(計算壓力),對於Mobile GPU,還意味著更快的Bining Pass(主要是頻寬壓力)。模型的減面、LOD包括normal map去代替高模體現細節都是同類優化。

減少每個頂點資料量

這個也比較直觀,資料量更少意味著VAF階段和Binning Pass階段更少的讀寫開銷。甚至有時候,我們可以在VS裡使用一些快速的頂點資料壓縮/解碼方案[21][22](少量的計算開銷換取更少的頻寬開銷)。

避免小三角形

小三角形最直觀的缺點就是:在螢幕上佔用的畫素非常少,是一種視覺上的浪費。實際上,由於硬體管線中,針對三角形有圖元裝配的環節(Triangle Setup),還有三角形的剔除(Vertex Culling/Triangle Clipping),因此主要是“構造一個三角形的固定開銷”。文章[19]中還提到了一定要避免小於32畫素的Triangle,我猜是因為小於32畫素的三角形在PS階段,組的Warp可能是不足32pixel的(有待考證)。近幾年提出的GPU Driven Pipeline裡面,已經有用Compute Shader去剔除小三角形的優化方法[23]。

優化索引緩衝

這是一個很少會有人提到的優化,原理是:VAF的階段,頂點的資料是根據PD派發的indices patch(長度是幾十個頂點索引)從視訊記憶體裡面取的,indices patch相當於把一個長的index buffer切分成小段,在每個indices patch內,同一頂點被訪問越多次,memory cache的命中率越高,相應地,頻寬開銷就越小。所以我們可以通過重排index buffer,讓一段indices patch內同一頂點被引用的次數儘可能地多[24]。

Interleaving Attributes vs.Seperate Attributes

通常來說,如果一堆屬性在VS中始終是會被一起使用(比如skinned weight和skinned indices;Normal/Tangent),我們應該把它們放在一起以減少Graphics API bind的次數,如果一堆屬性在不同VS中使用頻率相差很大(比如position非常頻繁,但vertex color很少使用),那麼我們應該儲存在不同buffer。這個原理和AoS/SoA的區別一樣,也是儘量提高快取的利用率(快取載入的時候的最小單位是Cache Line,通常64/128Bytes,所以要保證每次memory access能load更多有用的資料到cache)。

物件的優化

基於攝像機距離的排序/Z Pre-Pass

這個優化我們之前說過,對於有early-z機制的GPU(IMR/TBR)是有效的,對TBDR無效。

基於材質/RenderState的排序

RenderState是一個比較籠統的稱呼,對於OpenGL這類基於狀態機的Graphics API,像是buffer/texture繫結,framebuffer切換,shader切換,depth/stencil/culling mode/blend mode等都屬於狀態切換,並且有效能開銷。狀態切換中涉及的開銷包括driver端的命令驗證及生成;GPU內部硬體狀態機的重新配置;視訊記憶體的讀寫;CPU/GPU之間的同步等。這裡有一張圖[25]大致量化了各類狀態切換的開銷:

當我們談優化時,我們談些什麼

至於每個graphics API call在呼叫的背後發生了什麼,我沒有相關的知識去詳細解釋。這個主題也足夠一篇文章的內容去單獨闡述,如果有這方面的資料或者寫driver的朋友,希望可以解釋一下。

最理想狀況當然是把相同材質/RenderState的物體可以合併為一個batch提交,也就是我們常說的減少draw call[26]。

貼圖的優化

貼圖優化的核心只有一個:Cache Friendly。

減少貼圖尺寸

很多人都覺得減少貼圖尺寸帶來的最大優化是視訊記憶體,對於主機/移動平臺以及一些受視訊記憶體大小限制的場景或許是對的。但從效能角度分析,減少貼圖尺寸帶來的最大好處是提高快取命中率:假設把一張1024*1024的貼圖換成一張1*1的貼圖,shader不變,你會發現shader的執行速度變快了,因為對所有需要取樣這個貼圖的shader來說,真正從記憶體讀取資料只需要一次,而後的每次取樣,都只需要從cache裡取那個畫素資料即可。換句話說,我們關注的是每條cache line能夠覆蓋多少個畫素的PS執行。貼圖尺寸越小,每條cache line覆蓋的被取樣畫素就越多。

使用壓縮貼圖

這個思路和頂點壓縮是類似的,即犧牲一些計算量用於即時的資料解壓縮,來換取更少的頻寬消耗。諸如DXT/PVRTC/ASTC都是這樣的思路。同樣的思路還可以用在緊湊的G-Buffer生成,比如CryEngine曾經用Best Fit Normal和YCbCr色彩空間壓縮G-Buffer[27][28]。

合併貼圖到Texture Altas

這個其實是為了減少貼圖的繫結開銷,本質上是減少狀態切換。如果有Bindless Texture[25]的情況下,這個優化就幫助不大。

使用Mipmap

通常我們使用Mipmap是為了防止uv變化比較快的地方(一般是遠處)的貼圖取樣出現閃爍,但究其根源,閃爍是因為我們在對相鄰畫素進行著色的時候,採到的texel是不連續的。這其實就意味著cache miss。而Mipmaped texture會按級儲存每一層mip(實體記憶體上連續),這就意味著當你使用Mipmap去取樣的時候,快取命中率是更高的,因此效能相比沒有Mipmap的貼圖也會更好。

儲存結果到Buffer還是Texture?

有時候我們會把一些通用計算放在GPU上,結果存在buffer/texture上,理論上,如果能夠選擇的話,儘量把不是圖片型別的資料儲存在buffer上(比如particle的velocity/pos或者skinPallete,最好用buffer存)。這聽起來是一句廢話,理由是:Buffer和Texture在記憶體中的儲存佈局不一樣,Buffer是線性的,Textute是分塊的,在非貼圖資料的訪存模式下,分塊的記憶體佈局往往不利於快取命中。

Tips:當然,對於移動平臺來說,Cache Friendly還意味著更少的發熱。

Shader的優化

減少分支?

我們已經解釋過GPU是如何實現分支的,所以再回到是否要減少分支這件事,就不應該一味地認為分支總是對效能不好的。應該說,如果分支的結果依賴shader在執行時決定,並且這個結果在warp內差異很大,那麼我們應該避免分支,實在無法避免時,儘量提取公共計算部分到分支外。近年來大部分的Deferred Shading框架,都依賴於Material ID去判斷材質型別,並在shading階段依賴動態分支去做不同的著色計算,這是因為材質在螢幕空間上的變化是比較少的(大部分使用標準PBR材質),所以分支帶來的效能問題也不大。

另外,我們經常會用一些Uber Shader來實現不同的材質效果(但又共享很多公共計算)。實現的思路有兩種:用巨集定義基於Uber Shader生成不同的Sub Shader和Uber Shader內基於Uniform的分支。前者可能帶來的shader切換的開銷,後者反倒可能更有利於效能(當然,這個也要具體情況具體分析)。

精確指定資料型別

對於ALU來說,它的許多數學運算指令的時長/併發數是依賴於資料位寬的。因此應該儘量使用演算法允許的最低精度資料型別來進行計算,比如GLSL中,可以通過highp/mediump/lowp去指定當前shader的資料計算精度。另外,在進行Int/Float的混合計算時,需要額外的指令對Int型別進行轉換,因此,如無必要,儘量不要用Int類資料。

使用向量演算法還是標量演算法?

過去很多Shader Core的設計是Vector Based,即ALU只會進行向量的加減乘除,對於標量也會規約到向量運算。基於這類設計,就有一些奇技淫巧,去把一個計算儘量向量化[17]。但現在更多的是Scalar Based的Shader Core,所以也就無需太過關注這點,但是,我們還是應該儘量延遲向量和標量之間的運算,比如這個例子:

當我們談優化時,我們談些什麼

用貼圖快取中間計算結果?

很多時候,我們會把一些數學上的中間計算結果快取到一張貼圖裡,這些貼圖的數值本身不代表視覺資訊,而是純粹的數字。比如Marschner Hair Mode用LUT去存BRDF[29];UE4用LUT去儲存PBR的環境光BRDF[30]。

LUT帶來的效能損耗有兩點:

(1)貼圖本身是數值,所以只能用無損格式,無法壓縮,所以bytes per pixel是比較大的,比一般貼圖佔用更多讀取頻寬

(2)對於貼圖的取樣是基於LUT的uv計算的,而相鄰畫素算出的uv通常都沒有空間連續性,這就表示每次LUT的取樣幾乎都會導致cache miss,所以這類LUT比一般貼圖的取樣更慢。

結論:儘量使用擬合函式去代替LUT取樣,對於Mobile GPU來說,永遠不要嘗試用LUT去優化一段shader;對於Desktop GPU來說,慎重考慮使用LUT。

結語

這可能是我寫過最累的一篇專欄文章,快寫完的時候發現其實這個主題應該拆成三篇來寫。優化涉及的內容細碎又繁瑣,概念之間彼此相互關聯,而且大部分知識,我只會記下出處和大概內容,要寫下來的時候,往往還需要重新查詢引用並且確認細節。另一方面,這個主題相對來說比較硬核,硬體知識枯燥無味,比起圖形演算法來說要無趣得多,也沒人愛看。以至於寫到最後,我都開始懷疑到底為什麼要寫這個東西。

可能對我來說最大的意義,一是對過往雜亂的知識做一個自我梳理;二是告訴那些面試官,其實我也懂一點優化的(雖然只是嘴炮而已:))。

參考

1.^Life of a Triangle https://developer.nvidia.com/content/life-triangle-nvidias-logical-pipeline
2.^Practical DirectX 12 https://developer.nvidia.com/sites/default/files/akamai/gameworks/blog/GDC16/GDC16_gthomas_adunn_Practical_DX12.pdf
3.^What is a Texture Processor Cluster or TPC?https://www.geeks3d.com/20100318/tips-what-is-a-texture-processor-cluster-or-tpc/
4.^https://en.wikipedia.org/wiki/Branch_predictor
5.^Fermi Hardware&Performance Tips http://theinf2.informatik.uni-jena.de/theinf2_multimedia/Website_downloads/NVIDIA_Fermi_Perf_Jena_2011.pdf
6.^Analyzing GPGPU Pipeline Latency http://lpgpu.org/wp/wp-content/uploads/2013/05/poster_andresch_acaces2014.pdf
7.^FEATURES HARDWARE Nvidia DLSS:An Early Investigation https://www.techspot.com/article/1712-nvidia-dlss/
8.^GTX1080 is Here!https://zhuanlan.zhihu.com/p/20861061
9.^圖靈架構特性解析https://zhuanlan.zhihu.com/p/44644238
10.^一篇光線追蹤的入門https://zhuanlan.zhihu.com/p/41269520
11.^這是一篇光線追蹤的騙贊文https://zhuanlan.zhihu.com/p/51493136
12.^NVIDIA Turing Architecture In-Depth https://devblogs.nvidia.com/nvidia-turing-architecture-in-depth/
13.^https://www.nvidia.com/object/pascal-architecture-whitepaper.html
14.^https://www.nvidia.com/content/dam/en-zz/Solutions/design-visualization/technologies/turing-architecture/NVIDIA-Turing-Architecture-Whitepaper.pdf
15.^ARM Mali GPU Architecture https://armkeil.blob.core.windows.net/developer/Files/pdf/graphics-and-multimedia/Mali_GPU_Architecture.pdf
16.^PowerVR Hardware Architecture Overview for Developers http://cdn.imgtec.com/sdk-documentation/PowerVR+Hardware.Architecture+Overview+for+Developers.pdf
17.^abGPU architectures https://drive.google.com/file/d/12ahbqGXNfY3V-1Gj5cvne2AH4BFWZHGD/view
18.^abGPU Framebuffer Memory:Understanding Tiling https://developer.samsung.com/game/gpu-framebuffer
19.^abPowerVR Performance Recommendations http://cdn.imgtec.com/sdk-documentation/PowerVR.Performance+Recommendations.pdf
20.^PowerVR Performance Recommendations The Golden Rules http://cdn.imgtec.com/sdk-documentation/PowerVR+Performance+Recommendations.The+Golden+Rules.pdf
21.^完整的頂點壓縮http://www.klayge.org/2012/11/11/%E5%AE%8C%E6%95%B4%E7%9A%84%E9%A1%B6%E7%82%B9%E5%8E%8B%E7%BC%A9/
22.^壓縮tangent-frame http://www.klayge.org/2012/09/21/%E5%8E%8B%E7%BC%A9tangent-frame/
23.^Optimizing the Graphics Pipeline with Compute https://frostbite-wp-prd.s3.amazonaws.com/wp-content/uploads/2016/03/29204330/GDC_2016_Compute.pdf
24.^OpenGL Insights,30.WebGL Models:End-to-End
25.^abBeyond Porting http://www.ozone3d.net/dl/201401/NVIDIA_OpenGL_beyond_porting.pdf
26.^Batch,Batch,Batch https://www.nvidia.com/docs/IO/8228/BatchBatchBatch.pdf
27.^Rendering Technologies from Crysis 3 https://www.slideshare.net/TiagoAlexSousa/rendering-technologies-from-crysis-3-gdc-2013
28.^CryENGINE 3:Reaching the Speed of Light.
29.^GPU Gems 2,Chapter 23.Hair Animation and Rendering in the Nalu Demo https://developer.nvidia.com/gpugems/GPUGems2/gpugems2_chapter23.html
30.^Real Shading in Unreal Engine 4 https://cdn2.unrealengine.com/Resources/files/2013SiggraphPresentationsNotes-26915738.pdf

作者:洛城
專欄地址:https://zhuanlan.zhihu.com/p/68158277

相關文章