Labs 導讀
在開發圖形渲染應用時,渲染性能優化是一個繞不開的主題,開發者往往遵循一些優化準則來構建自己的應用程序,包括數據合并、模型減面、減少采樣次數、減少不必要渲染等。本文結合現代GPU架構及邏輯管線執行,簡單闡述這些性能優化背后的原理。
Part 01、 現代GPU架構
早期GPU設計遵循硬件渲染管線理念,管線的每個功能階段都有對應的硬件單元實現,這種設計導致整個渲染管線是固定功能的,開發人員無法做更多地更改,只能通過圖形API實現相應的功能,例如早期OpenGL提供圖形接口實現光照的設置。為服務更廣泛的科技業務需求,現代GPU設計則更加靈活,遵循邏輯渲染管線的理念,引入可編程部分,硬件單元得以復用以實現管線的每個功能階段。本文以抽象的Fermi架構來闡述現代GPU結構,如下圖所示。
主機接口(Host Interface)是GPU與CPU溝通的橋梁,用于進行數據和指令的交換。大規模線程引擎(Giga Thread Engin)扮演大管家的角色,管理GPU中執行的所有工作,包括線程塊與線程束調用,并行度調整等。核心工作部分則是圖形處理集群(Graphics Processing Cluster),即GPC,負載執行圖形渲染任務,一個GPU的內部可以有多個GPC,單個GPC內部抽象結構如下圖所示。
GPC中主要包含一個光柵化引擎(Raster Engine)和多個流式多處理器(Streaming Multiprocess, 即SM)。Raster Engine主要負責將圖元數據轉換為屏幕上的像素,SM主要用于執行開發人員編寫的著色器代碼,內部包含多個數學運算核心。SM的抽象結構如下圖所示。
幾何處理引擎(Poly Morph Engine)主要進行幾何處理和數據準備工作,在下述邏輯管線執行部分將介紹其部分功能。SM中緩存主要包括:
1??指令緩存(Instruction Cache),用于存儲指令及指令所需的數據。
2??共享內存(Shared Memory),用于管線不同功能階段數據的存儲與傳遞。
3??Uniform變量緩存(Unifrom Cache),用于存儲共享的Uniform變量數據,以便多個執行線程高效訪問這些數據。
4??紋理緩存(Texture Cache),用于緩存紋理數據,提高訪問紋理數據的速度。
SM中的計算執行部分主要包含線程束調度(Wrap Scheduler),分發單元(Dispatch Unit)以及32個計算核心(Core)。Wrap Scheduler負責線程束(wrap)的調度,一個wrap包含32個線程,這些線程的指令被提交給分發單元(Dispatch Unit),由Dispatch Unit分發給各個Core執行,指令以鎖步(lock-step)方式執行,即一個wrap中所有線程按照相同的控制流路徑同時執行一個指令(單指令多線程)。
Part 02、 邏輯管線執行
上圖是簡化的邏輯管線執行過程,可分為CPU和GPU階段。在CPU部分,開發者利用圖形API構建應用程序,通過drawcall發出指令,這些指令會被推送給驅動,驅動程序首先會進行指令合法性檢測,然后將其存儲到Push Buffer中。
在GPU部分,接受到繪制請求后,GPU中的Host Interface會接受到這些指令數據,并交由Front End進行分析處理,處理后的數據會發送給圖元分發器(Primitive Distributor),Primitive Distributor會把頂點數據組織成圖元數據形式,并將這些數據按批次發送給各個GPC。
數據和指令的轉送則是通過交叉柵(Cross Bar)進行的。首先進行幾何階段任務,上述SM中的Poly Morph Engine會執行Vertex Fetch功能,即獲取頂點數據,然后依次執行頂點著色器代碼(Vertex Shader)和幾何著色器代碼(Geometry Shader),這一過程則是上述提到線程指令在計算核心中以lock-step方式進行,最后Poly Morph Engine會進行視口變化(View Transform),為光柵化做準備,丟棄不在視口范圍內的頂點。
光柵化階段主要進行光柵化、片段著色器(Fragment Shader)執行以及逐片元處理。Raster Engine完成對視口內頂點數據的光柵化,Poly Morph Engine會負責屬性設置(Attribute Setup),以方便光柵化時屬性數據的插值采用片段著色器友好格式。Fragment Shader執行與上述Vertex Shader執行一樣,唯一不同是Vertex Shader是按頂點并行進行的,而Fragment Shader是按像素并行進行的。Fragment Shader產生的結果通過Cross Bar傳給渲染輸出單元(Render Output Unit),Render Output Unit會以原子方式進行逐片元處理,包括模版測試、深度測試、像素混合等。最終生成的結果被存儲在幀緩沖(Framebuffer)中。
Part 03、 性能優化
在開發圖形應用時,開發者往往需要遵從一些渲染性能優化原則編寫自己的程序。結合上述GPU架構與邏輯管線執行流程,依次闡述其中的原理。
- 減少drawcall
從上述的流程執行可以看出,渲染的過程是復雜的,渲一個三角行與渲染多個三角行執行的過程是一致的,為了發揮GPU強大的并行能力,需要開發者在每次繪制時,向GPU發送足夠的渲染數據,以便最大限度的利用GPU。其次,drawcall并不是直接繪制,而是將指令與數據發送給GPU,過多的drawcall會增加CPU與GPU的通信開銷。上述PushBuffer可以減少CPU與GPU的通信開銷,CPU寫入指令,當PushBuffer中填充完成,CPU將整個PushBuffer一次性發送給GPU,減少CPU與GPU間的通信次數。實踐中,可以采用網格數據合并、實例繪制等方式減少drawcall調用。
- 減少紋理采樣次數
采樣是指從紋理中獲取像素顏色的過程。紋理采樣需要從GPU內存中讀取紋理數據,這是一個相對較慢的過程,讀取跟不上運算速度從而導致延遲。在GPU中,為了處理由于數據沒準備好而引起的線程執行延遲,Wrap調度器會掛起當前延遲的Wrap,選擇可立即執行的Wrap執行。在SM中存在Texture Cache,以緩存紋理數據,提高采樣效率。在實踐中,可通過多重采樣(multisample)實現反走樣,但由于采樣次數的增加,渲染性能也會下降。
- 減少模型頂點數
頂點數據的處理主要在幾何階段,Vertex Shader的執行是按照頂點并行的,計算核心的個數是固定,頂點越少,所需執行線程的越少,完成所有線程執行花費的時間也就越少。在實踐中,可以采用低精度模型結合法向貼圖的模型替代高精度模型,也可使用LOD技術動態切換不同精度的模型。
- 避免著色器中的分支語句
著色器代碼指令是按照lock-step方式執行的,假設著色器代碼中存在if-else語句,在一個Wrap中有32個線程,其中只有1個線程條件為真執行if語句,剩下31個線程均執行else語句,在執行if語句時,剩下31個線程會等待,當31個線程執行else語句時,執行if的線程會等待,即相當于每個線程if與else語句均執行了一次,整體執行流程如下圖所示。在實踐中,可以利用著色器提供的step函數來規避分支語句的編寫。
- 減少不必要渲染
實踐中運用較多的技術是遮擋剔除與Early z。遮擋剔除一般是在CPU端判斷物體是否在場景的虛擬視線范圍內,以剔除不在視線范圍內的物體,減少不必要的渲染。Early z則是現代GPU硬件所支持的優化技術,當光柵化結束,Raster Engine會進行Early z,比較片元深度值,剔除那些在深度方向被遮擋的片元,以減少后續片元著色器的工作量,Early z類似于提前進行了ROP階段的深度測試。
Part 04、 結束語
本文結合架構與管線執行簡單闡述了一些性能優化準則的原理,了解現代GPU架構與邏輯管線執行有利于開發者構建高性能應用。