分為三個部分:Unity官方文檔,GDC,個人經驗。
Unity Manual
1.計算量優化。着色器進行的計算和處理越多,對性能的影響越大。針對不影響最終效果但依然進行計算的無效代碼,進行移除操作。計算的頻率也會影響游戲的性能。通常,像素着色器比頂點着色器的執行次數要多。在可能的情況下,將計算從像素着色器移動到頂點着色器,或將它們完全在着色器移除,在腳本中計算並傳遞給着色器。
2.表面着色器優化。Unity提供的表面着色器非常適合編寫與光照交互的着色器。針對特定情況設置關鍵字以使着色器效率更高或減小體積:
approxview使用逐頂點而不是逐像素的規范化觀察向量
。雖然是近似值,通常足夠使用。鏡面反射着色器類型使用halfasview更快
。計算半角向量並逐頂點進行規范化,並且光照函數使用半角向量而不是觀察向量作為參數。noforwardadd
着色器僅完全支持前向渲染中的一個定向光源。其余的光源仍然可以有逐頂點光照或球諧的效果。減少了着色器大小,即使存在多個燈光始終只渲染一次。noambient
禁用着色器的環境光和球諧光照。
3.計算精度優化。當使用CG / HLSL編寫着色器時,存在三種基本的數字類型:float(32bits)
,half(16bits)
和fixed(11bits)
- 對於世界空間位置和紋理坐標,使用
float
精度。 - 對於其他一切(矢量,HDR顏色等),首先使用
half精度
,必要時增加精度。 - 對於紋理數據的非常簡單的操作,使用
fixed
精度。
實際上,應該使用哪種數據精度取決於平台和GPU。一般來說:
- 所有現代桌面級GPU總是以完全
float精度進行
計算,float/half/fixed
在底層是完全相同的。因此在Unity編輯器中(即使切換為移動平台),難以確定半/固定精度是否足夠,因此請始終在目標設備上測試着色器以獲得准確的結果。 - 移動GPU具有實際
half精度
支持。通常更快,並且使用更少的功率來進行計算。 Fixed
精度通常僅對較舊的移動GPU有效。大多數現代GPU(支持OpenGL ES 3.0或Metal)內部處理fixed
和half
精度完全相同。
4.Alpha Testing優化。固定函數AlphaTest - 或其可編程等價函數clip()
- 在不同平台上具有不同的性能表現:
- 通常,在大多數平台上使用它來移除的完全透明像素時,有些許性能優勢。
- 但是,在iOS和某些使用PowerVR GPU的Android設備上,alpha testing是資源密集型的。不要試圖在這些平台上使用它進行性能優化,會導致游戲運行速度比平常慢。
5.Color Mask優化。在某些平台上(主要是iOS和Android設備中的移動GPU),使用ColorMask忽略某些通道(例如ColorMask RGB
)可能是資源密集型的,因此請在必要時才使用。
GDC
GDC2013和GDC2014上介紹了DX10和DX11上PC和Console上的底層着色語言優化,將優化放在減少着色器指令數量上面。
GDC2013:http://www.humus.name/Articles/Persson_LowLevelThinking.pdf
GDC2014:http://www.humus.name/Articles/Persson_LowlevelShaderOptimization.pdf
個人認為,在PC和Console平台上對於指令數量的優化意義並不大。但是在移動平台,指令數量的優化還是有必要。雖然SM3.0指令數量已經基本不會對着色器編寫復雜度進行限制,但是如果要求支持SM2.0,96條指令數量要求十分嚴苛。
由於着色器指令的優化與硬件(HW)關系密切,因此我們需要根據硬件廠商提供的相關文檔進行優化。移動平台的三大GPU品牌,分別是PowerVR,Mali,Adreno。PowerVR有專門的GLSL優化文檔,Mali和Adreno也有相關文檔提到這部分內容。
但這樣做,必然會增加着色器變體數量,因為我們要使用關鍵字來選擇執行不同的代碼,這會生成不同的着色器變體。
根據PowerVR Low Level GLSL Optimisation,我這里列舉一些優化的方式。至於Mali和Adreno的優化,需要參考其開發文檔進行。
通常來講,在PowerVR上的Shader性能取決於執行Shader的周期次數。PowerVR Rogue架構提供了多種選擇用於在USC ALU管線中的單一周期執行多個指令。
從下圖可以看到,在一個周期內,可以執行最多三個Phase。為了高效的利用ALU,按照下面的規則,重新排列我們的GLSL代碼是明智的。
指令優化
1.MAD
上圖可以知道,MAD和MUL/ADD均占用一個Phase,但是MAD卻執行了a*b+c的計算,這想當於一個Phase執行了一次乘法和加法。將表達式是改為MAD形式,會減少50%的周期消耗。
2.Division
將除法寫為乘以除數的倒數(rcp)的形式,對優化有幫助。同樣的,簡化表達式也會獲得額外的性能增加。
3.Sign
sign(x)的計算是這樣的:返回 -1 if x<0; 0 if x =0;1 if x >0.
如果不需要x=0的情況,那么最好的方式是自己實現。
4.Rcp/Rsqrt/Sqrt
在PowerVR Rogue架構中,倒數操作是直接一條指令支持的。
rsqrt()也同樣是硬件支持的。
sqrt()在另一方面是以1/(1/sqrt(x))的方式實現的,因此它占用兩個循環。
一般來說用替代的實現x*1/sqrt(x)實現sqrt的功能。
同樣是兩個周期,使用替代實現更好的唯一情形是結果會被測試。在這種情況下,測試指令剛好放入第二條指令中。
5.Abs/Neg/Saturate
在PowerVR架構中,修飾符如abs(),neg()和clamp(…,0.0,1.0)(相當於saturate())的優勢是很重要的,因為在特定的情況下,他們沒有消耗。abs()和neg()如果用於操作的輸入,是無消耗的。在這種情況下,他們被編譯器轉換成無消耗的修飾。saturate()相反,當用於操作的輸出的時候,被轉換為無消耗的修飾。
但是對於復雜的或者采樣/插值指令卻不符合這個規則。換句話說,對於紋理采樣輸出,或者復雜的指令輸出,saturate()並不是無消耗的。當這些函數沒有使用時,它們可能會引入額外的mov指令,這些指令可能會影響着色器的循環計數。
使用clamp(…,0.0,1.0)而不是min(…,1.0)和max(…,0.0)也有利於優化。這令原有的測試指令變為saturate修飾符。
之后,對於復雜函數,他們被譯為多個操作並且因此在這個情形下,修飾符的位置就十分重要。比如,規范化函數normalize(),它的實現。
正如所看到的,在這種情況下,最好是對最終乘法的一個輸入取負,而不是所有情況下的都對輸入取負,或者創建一個臨時的負值輸入:
6.Exp/Log
在PowerVR Rogue架構,2^n操作是一條指令支持的操作。
Log2()同樣。
Exp()與Exp2()的實現不同,占用兩個循環。
Pow(x,y)的實現如下,需要三個周期。
7.Sin/Cos/Sinh/Cosh
Sin,Cos,Sinh,Cosh在PowerVR架構上有適度的四個周期的低消耗。它們被分解為fred*2+fsinc+一個條件。
8.Asin/Acos/Atan/Degrees/Radians
如果實現了數學表達式的簡化,之后的這些函數通常不會被用到。因此,它們並不會精確的映射到硬件。這意味着這些函數有者非常高的消耗,並且在任何時候都應該避免使用。
Asin()耗費多達67個周期。
Acos()耗費多達79個周期。
Atan()依然比較耗,但是如果需要的話還是可以使用的。
雖然degrees和radians只有一個周期,但如果只使用弧度進行計算,通常是可以避免的。
9.Vector*Matrix
Vector*Matrix有一個相對比較合理的開銷,不管需要發生的計算數量。優化例如會知道w=1的優勢,但並不會降低開銷。
10.Mixed Scalar/Vector math
Normalize()/length()/distance()/reflect()等函數內部通常會包含許多的函數調用例如dot()。知道這些函數是如何實現的是一個優勢。
例如,如果我們知道兩個操作有共享的子表達式,我們可以減少周期數量。然而,這只在如果輸入順序允許的情況下發生。
手動的展開這些復雜的函數有時可以幫助編譯器優化代碼。
同樣的,在展開形式組合向量和標量指令也可能得到優化。
下面列舉一些復雜指令的展開形式。
cross()可以擴展為:
distance()可以擴展為:
dot()可以擴展為:
faceforward()可以擴展為:
length()可以擴展為:
normalize()可以擴展為:
reflect()可以擴展為:
refract()可以擴展為:
11.Operation grouping
將標量和向量操作分別組合是有利於優化的。這樣編譯器可以將更多的操作打包到單一的周期中。
FP16概述
1.FP16精度和轉化
當簡化的精度滿足的時候,FP16的管線工作的不錯。然而,依然建議經常檢查優化后的結果會不會出現精度瑕疵。當16位的浮點精度硬件可用,並且着色器使用中精度的時候,16位與32位的轉化使用修飾符是無開銷的,因為被USC ALU管線包含了它。
然而,當着色器不使用16位指令或者例如早期的Rogue硬件硬件不包含16位浮點管線,指令只會在常規的32位管線執行並且因此也不會有轉化發生。
2.FP6 SOP/MAD operation
FP16 SOP/MAD管線是PowerVR ALU管線最強大的地方之一。如果使用得當。它允許開發者打包更多到操作到單一周期中。這可以提高性能並降低功率消耗。
單一周期的FP16 SOP/MAD操作可以被以下的偽代碼描述:
輸入應用各種修飾符(abs(),negete(),oneminus())和輸出應用clamp()也是合適的。下一小節介紹如何完全的利用FP16 SOP/MAD管線。
3.利用FP16 SOP/MAD管線
PowerVR Rogue架構有一個強大的為常規圖形操作優化的FP16管線。這節描述了如何利用FP16管線。注意轉換輸入到FP16之后轉換輸出到FP32是零開銷很重要。
對SOP/MAD你有許多的選擇。在一個周期中,你可以執行2個SOP操作或2個MAD操作或1個MAD+1個SOP操作。二選一的,你可以在單一周期執行4個FP16 MAD操作。
在單一周期執行4個MAD:
SOP表示乘法的結果之間選擇一個操作的點積之和:
這里OP可以是一個加法,減法,min()或者max():
你也可以對輸入應用取負,abs()或者clamp()(saturate)中的任一:
最后,你也可以對最終結果應用clamp()(saturate):
在應用完所有的知識之后,我們可以通過使用單一周期的任何事情來炫耀我們的管線功率:
經驗
1.將一些計算烘焙到紋理
具體來講,就是使用紋理讀取的方式減輕運算量。在一般硬件,采樣操作占用一個周期。如果將BRDF的D/F/G等計算烘焙到一張RGBA的四個通道中,我們只需要計算輸入的幾個點積結果以及粗糙度等參數,通過采樣紋理得到計算結果(或者是中間結果)。這部分的操作比較靈活。在不追求計算精確的情況下,以空間換時間。
2.計算轉移的思考
由Unity的官方手冊優化建議第一條,我們可以知道,在Shader編寫中,我們可以知道有些計算,在不影響表現的情況下,是可以放到三個部分中的,頂點着色器,片段着色器以及腳本中。頂點着色器和片段着色器,分別是逐頂點和逐像素來進行計算的。而腳本中,是每一幀計算的(Update函數)。
那把計算放到哪里更能優化性能?Unity的官方文檔給出的建議的腳本優於頂點着色器優於像素着色器。
在不考慮剔除的情況下:
頂點着色器:逐頂點計算,計算次數等於頂點數。
片段着色器:逐像素計算,計算次數等於像素數。
腳本:逐幀計算,每幀計算一次。
從計算次數上來看效率,腳本>頂點着色器>片段着色器。
然而區別是,頂點着色器和片段着色器是Shader內部的計算,運行在GPU上。計算從片段着色器移動至頂點着色器使得性能得到優化是沒有疑問的。而腳本的計算則是運行在CPU上,計算得到的結果傳遞給GPU也會有性能的開銷。
舉兩個極端的例子:
模型1:超高精度模型,模型頂點數1000W。(Nvidia的技術展示Demo)
模型2:Quad,模型頂點數4。