

下載提醒:《人工智慧晶片技術深度分析》採用圖文並茂,深入淺出,內容包括人工智慧晶片概述、人工智慧晶片關鍵技術、GPU晶片技術分析、FPGA晶片技術分析、ASIC晶片技術分析、類腦晶片分析、人工智慧晶片專利分析、人工智慧晶片全球市場分析、人工智慧晶片在國內發展情況等7個章節。
知識全解系列
(持續更新中…)
資料中心網路知識全解(PPT)
人工智慧基礎知識全解(含實踐)
CPU基礎知識全解(PPT)
GPU基礎知識全解(PPT)
渲染技術總是伴隨著顯示卡硬體的升級而發展的,從最初的GeForce 256開始支援T&L,到RTX支援光追,硬體和渲染技術都在不斷更新。作為軟體技術開發人員,平時更多是從軟體視角去理解渲染。為了更進一步瞭解渲染的本質,本文換一個視角,收集並整理了一些資料,從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內部的。
它們的存取速度從暫存器到全域性記憶體依次變慢:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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中使用到的暫存器不光這些“語義”的暫存器,它們分為很多種型別,包括輸入暫存器、常量暫存器、臨時暫存器等。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
資料進入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指令集。
例如下面的程式碼。
如果沒有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的特性,暫存器要儘可能完全利用。例如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. fetch(獲取指令)
-
2. decode(解碼指令)
-
3. execute(執行指令)
-
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]



免責申明:本號聚焦相關技術分享,內容觀點不代表本號立場,可追溯內容均註明來源,釋出文章若存在版權等問題,請留言聯絡刪除,謝謝。
推薦閱讀
更多架構相關技術知識總結請參考“架構師全店鋪技術資料打包(全)”相關電子書(44本技術資料打包彙總詳情可透過“閱讀原文”獲取)。
全店內容持續更新,現下單“架構師技術全店資料打包彙總(全)”一起傳送“伺服器基礎知識全解(終極版)”和“儲存系統基礎知識全解(終極版)”pdf及ppt版本,後續可享全店內容更新“免費”贈閱,價格僅收259元(原總價499元)。
溫馨提示:
掃描二維碼關注公眾號,點選閱讀原文連結獲取“架構師技術全店資料打包彙總(全)”電子書資料詳情。

