【《Real-Time Rendering 3rd》 提煉總結】(十二) 渲染管線優化方法論:從瓶頸定位到優化策略


本文由@淺墨_毛星雲 出品,首發於知乎專欄,轉載請註明出處。  
文章鏈接: https://zhuanlan.zhihu.com/p/32928016


這是一篇很特殊的文章。它將會是這個系列文章主線的最後一篇。

不知不覺中,專欄中【《Real-Time Rendering 3rd》 提煉總結】系列文章已經增加到了十二篇。從2017年寫到了2018年,從渲染管線、高級着色、延遲渲染,一路寫到全局光照、光線追蹤、非真實感渲染,到這篇文章的渲染管線優化。

相信不僅是我自己在閱讀和總結“實時渲染的聖經”《Real-Time Rendering 3rd》的過程中受益良多,一路閱讀這個系列文章的朋友們,也應該是頗有收穫。

感謝大家一直以來的支持與陪伴。




導讀


這篇文章約1萬8千字,構成主要分爲上篇(渲染管線瓶頸定位策略),下篇(渲染管線優化策略),以及常用的性能分析工具的列舉三部分,詳細目錄如下。

    • 一、渲染管線的構成
    • 二、渲染管線優化策略概覽
    • 上篇:渲染管線的瓶頸定位
      • 3.1 光柵化階段的瓶頸定位
        • 3.1.1 光柵化操作的瓶頸定位
        • 3.1.2 紋理帶寬的瓶頸定位
        • 3.1.3 片元着色的瓶頸定位
      • 3.2 幾何階段的瓶頸定位
        • 3.2.1 頂點與索引傳輸的瓶頸定位
        • 3.2.2 頂點變換的瓶頸定位
      • 3.3 應用程序階段的瓶頸定位
    • 下篇:渲染管線的優化策略
      • 4.1 對CPU的優化策略
        • 4.1.1 減少資源鎖定
        • 4.1.2 批次的尺寸最大化
      • 4.2 應用程序階段的優化策略
        • 4.2.1 內存層面的優化
        • 4.2.2 代碼層面的優化
      • 4.3 API調用的優化策略
      • 4.4 幾何階段的優化策略
        • 4.4.1 減少頂點傳輸的開銷
        • 4.4.2 頂點處理的優化
      • 4.5 光照計算的優化策略
      • 4.6 光柵化階段的優化策略
        • 4.6.1 加速片元着色
        • 4.6.2 減少紋理帶寬
        • 4.6.3 優化幀緩衝帶寬
    • 主流性能分析工具列舉
    • 更多性能優化相關資料

文中列舉了渲染管線各個階段中用到的幾十種主流的優化策略。其中,個人印象比較深刻的優化方法有使用實例(Instance)結合層次細節和impostors方法來對多人同屏場景的渲染進行優化,以及使用紋理頁(Texture Pages)來進行批次的尺寸最大化。

這篇文章會是《Real-Time Rendering 3rd》第十五章“Pipeline Optimization”和《GPU Gem I》第28章“Graphics Pipeline Performance”的一個結合,而不是之前一貫的《Real-Time Rendering 3rd》的單篇章節爲主線。

需要吐槽的是,如果你對照閱讀《GPU Gem I》的英文原版和中文翻譯版,會發現中文翻譯版中有一些不合理甚至曲解英文原文意思的地方,在第五部分性能與實這一部分尤其明顯。


OK,正文開始。



一、渲染管線的構成


通常,可以將渲染管線的流程分爲CPU和GPU兩部分。下圖顯示了圖形渲染管線的流程,可以發現,在GPU中存在許多並行運算的功能單元,本質上它們就像獨立的專用處理器,其中存在許多可能產生瓶頸的地方。包括頂點和索引的取得、頂點着色(變換和照明,Transform & Lighting,即T&L)、片元着色和光柵操作( Raster Operations ,ROP)。


圖1 圖形渲染管線


如《Real-Time Rendering 3rd》第二章所述, 圖形的渲染過程基於由三個階段組成的管線架構:

    • 應用程序階段(The Application Stage)
    • 幾何階段(The Geometry Stage)
    • 光柵化階段(The Rasterizer Stage)


基於這樣的管線架構,其中的任意一個階段,或者他們之間的通信的最慢的部分,都可能成爲性能上的瓶頸。瓶頸階段會限制渲染過程中的整個吞吐量,從而影響總結渲染的性能,所以不難理解,瓶頸的部分便是進行優化的主要對象。


圖2 渲染管線架構


若有對渲染管線架構不太熟悉的朋友,具體可以移步回看這個系列的第二篇文章《【《Real-TimeRendering 3rd》 提煉總結】(二) 第二章 · 圖形渲染管線 The Graphics Rendering Pipeline



二、渲染管線的優化概覽


準確定位瓶頸是渲染管線優化的關鍵一步。若沒有很好確認瓶頸就進行盲目優化,將造成大量開發的工作的無謂浪費。

根據以往的優化經驗,可以把優化的過程歸納爲以下基本的確認和優化的循環:

    • Step 1. 定位瓶頸。對於管線的每個階段,改變它的負載或計算能力(即時鐘速度)。如果性能發生了改變,即表示發現了一個瓶頸。
    • Step 2. 進行優化。指定發生瓶頸的階段,減小這個階段的負載,直到性能不再改善,或者達到所需要的性能水平。
    • Step 3. 重複。重複第1步和第2步,直到達到所需要的性能水平。


需要注意的是,在經過一次優化步驟後,瓶頸位置可能依然在優化前的位置,也可能不在。比較好的想法是,儘可能對瓶頸階段進行優化,保證瓶頸位置能夠轉移到另外一個階段。在這個階段再次成爲瓶頸之前,必須對其他階段進行優化處理,這也是爲什麼不能在一個階段上進行過多優化的原因。

同一幀畫面中,瓶頸位置也有可能改變。由於某個時候要渲染很多細小的三角形,這個時候,幾何階段就可能是瓶頸;在畫面後期,由於要覆蓋屏幕的大部分三角形單元進行渲染,因此這時光柵階段就可能成爲瓶頸。因此,凡涉及渲染瓶頸問題,即是指畫面中花費時間最多的階段。

在使用管線結構的時候應該意識到,如果不能對最慢的階段進行進一步優化,就要使其他階段與最慢階段的工作負載儘可能一樣多(也就是既然都要等瓶頸階段,不妨給其他階段分配更多任務來改善最終的表現,反正是要等)。由於沒有改變最慢階段的速度,因此這樣做並沒有改變最終的整個性能。例如,假定應用程序階段成爲瓶頸,需要花費50ms,而其他階段僅需要花費25ms。這意味着,在不改變管線渲染速度(50ms,即每秒20幀)的情況下,幾何階段和光柵化階段可以在50ms內完成各自任務。這時,可以使用一個更高級的光照模型或者使用陰影和反射來提高真實感(在不增加應用程序階段工作負載的前提下)。

管線優化的一種大致思路是,先將渲染速度最大化,然後使得非瓶頸部分和瓶頸部分消耗同樣多的時間(如上文所述,這裏的思想是,既然要等,不等白不等,不妨多給速度快的部分分配更多工作量,來達到更好的畫面效果)。但這種想法已經不適於不少新架構,如XBOX 360,因其爲自動加載平衡計算資源。

因爲優化技術對於不同的架構有很大的不同,且不要過早地進行優化。在優化時,請牢記如下三句格言:

    • “KNOW YOUR ARCHITECTURE(瞭解你所需優化的架構)”
    • “Measure(去測量,用數據說話)”
    • “We should forget about small efficiencies, say about 97% of the time: Premature optimization is the root of all evil.”(我們應該忘記一些小的效率,比如說97%的時間:過早的優化是萬惡之源。)- Donald Knuth


OK,下面開始,本文的上篇,渲染管線的瓶頸定位。



三、上篇:渲染管線的瓶頸定位策略


正確定位到了瓶頸,優化工作就已完成了一半,因爲可以針對管線上真正需要優化的地方有的放矢 。

提到瓶頸定位,很多人都會想到Profiler工具。Profiler工具可以提供API調用耗時的詳細信息,由此可以知道哪些API調用是昂貴費時的,但不一定能準確地確定管道中哪些階段正在減慢其餘部分的速度。(PS:本文文末提供了一系列常用的profiler工具的列表)

確定瓶頸的方法除了用Profiler查看調用耗時的詳細信息這種衆所周知的方法外,也可以採用基於工作量變化的控制變量法。設置一系列測試,其中每個測試減少特定階段執行的工作量。如果其中一個測試導致每秒幀數增加,則已經找到瓶頸階段。

而上述方法的排除法也同樣可行,即在不降低測試階段的工作量的前提下減少其他階段的工作量。如果性能沒有改變,瓶頸就是工作負載沒有改變的此階段。

下圖顯示了一個確認瓶頸的流程圖,描述了在應用程序中精確定位瓶頸所需要的一系列步驟。


圖3 確認渲染管線瓶頸流程圖 @ 《GPU GEMS I》


整個確認瓶頸的過程從渲染管線的尾端,光柵化階段開始,經過幀緩衝區的操作(也稱光柵操作),終於CPU(應用程序階段)。雖然根據定義,某個圖元(通常是一個三角形)只有一個瓶頸,但在幀的整個流程中瓶頸有可能改變。因此,修改流水線中多個節點的負載常常會影響性能。例如,少數多邊形的天空包圍盒經常受到片元着色或幀緩衝區存取的限制:只映射爲屏幕上幾個像素的蒙皮網絡時常受到CPU或頂點處理的約束。因此,逐個物體地改變負載,或逐個材質地改變負載時常是有幫助的。

另外,管線的每個階段都依賴於GPU頻率(分爲GPU Core Clock ,GPU核心頻率,以及GPU Memory Lock,GPU顯存頻率),這個信息可以配合工具 PowerStrip(EnTech Taiwan 2003),減小相關的時鐘速度,並在應用中觀察性能的變化。


下文將按照按照優化定位的一般順序(即上述圖中的流程),按光柵化階段、幾何階段、應用程序階段的的順序來依次介紹瓶頸定位的方法與要點。



3.1 光柵化階段的瓶頸定位


衆所周知,光柵化階段由三個獨立的階段組成: 三角形設置,像素着色器程序,和光柵操作。

其中三角形設置階段幾乎不會是瓶頸,因爲它只是將頂點連接成三角形。而測試光柵化操作是否是瓶頸的最簡單方法是將顏色輸出的位深度從32(或24)位減少到16位。如果幀速率大幅度增加,那麼此階段瓶頸。

一旦光柵化操作被排除,像素着色器程序的是否是瓶頸所在可以通過改變屏幕分辨率來測試。如果較低的屏幕分辨率導致幀速率明顯上升,像素着色器則是瓶頸,至少在某些時候會是這樣。當然,如果是渲染的是LOD系統,就需斟酌一下是否瓶頸確實是像素着色器了。

另一種方法與頂點着色器程序所採用的方法相同,可以添加更多的指令來查看對執行速度的影響。當然,也要確保這些額外的指示不會被編譯器優化。


下文將對光柵化階段三個常常可能是瓶頸的地方進行進一步論述。



3.1.1 光柵化操作的瓶頸定位


光柵化操作的瓶頸主要與幀緩衝帶寬(Frame-Buffer Bandwidth)相關。衆所周知,位於管線末端的光柵化操作(Raster

Operations,常被簡稱爲ROP),用於深度緩衝和模板緩衝的讀寫、深度緩衝和模板緩衝比較,讀寫顏色,以及進行alpha 混合和測試。而光柵化操作中許多負載都加重了幀緩衝帶寬負載。

測試幀緩衝帶寬是否是瓶頸所在,比較好的辦法是改變顏色緩衝的位深度,或深度緩衝的位深度(也可以同時改變兩者)。如果此操作(比如將顏色緩衝或深度緩衝的深度位從32位減少到16位)明顯地提高了性能,那麼幀緩衝帶寬必然是瓶頸所在。

另外,幀緩衝帶寬也與GPU顯存頻率(GPU memory clock)有關,因此,修改該頻率也可以幫助識別瓶頸。


3.1.2 紋理帶寬的瓶頸定位


在內存中出現紋理讀取請求時,就會消耗紋理帶寬(Texture Bandwidth)。儘管現代GPU的紋理高速緩存設計旨在減少多餘的內存請求,但紋理的存取依然會消耗大量的內存帶寬。

在確認光柵化操作階段是否是瓶頸所在時,修改紋理格式比修改幀緩衝區的格式更麻煩。所以,比較推薦使用大量正等級mipamap細節層次(LOD)的偏差,讓紋理獲取訪問非常粗糙的mipmap金字塔級別,來有效地減小紋理尺寸。同樣,如果此修改顯著地改善性能,則意味着紋理帶寬是瓶頸限制。

紋理帶寬也與GPU顯存頻率相關。


3.1.3 片元着色的瓶頸定位


片元着色關係到產生一個片元的實際開銷,與顏色和深度值有關。這就是運行”像素着色器(Pixel Shader )“或”片元着色器(Fragment Shader )“的開銷。片元着色(Fragment shading)和幀緩衝帶寬(Frame-Buffer Bandwidth)由於填充率(Fill Rate)的關係,經常在一起考慮,因爲他們都與屏幕分辨率相關。儘管它們在管線中位於兩個截然不同的階段,區分兩者的差別對有效優化至關重要。

在可編程片元處理的高級GPU出現之前,片元着色幾乎沒有什麼侷限性,時常是幀緩衝帶寬引起的屏幕分辨率和性能之間不可避免的瓶頸。但隨着開發者利用新的靈活性製造出一些新奇的像素,片元着色的性能問題也就出現了。

改變分辨率是確定片元着色是否爲瓶頸的第一步。因爲在上述光柵化操作步驟中,已經通過改換不同的深度緩衝位,排除了幀緩衝區帶寬是瓶頸的可能性。所以,如果調整分辨率使得性能改變,片元着色就可能是瓶頸所在。而輔助的鑑別方法可以是修改片元長度,看這樣是否會影響性能。但是要注意,不要添加可以被一些“聰明”的設備驅動輕鬆優化的指令。

片元着色的速度與GPU核心頻率有關。



3.2 幾何階段的瓶頸定位


幾何階段是最難進行瓶頸定位的階段。這是因爲如果在這個階段的工作負載發生了變化,那麼其他階段的一個或兩個階段的工作量也常常發生變化。爲了避免這個問題,Cebenoyan [1] 提出了一系列的試驗工作從光柵化階段後的管線。

在幾何階段有兩個主要區域可能出現瓶頸:頂點與索引傳輸( Vertex and

Index Transfer)和頂點變換階段(Vertex Transformation Stage)。要看瓶頸是否是由於頂點數據傳輸的原因,可以增加頂點格式的大小。這可以通過每個頂點發送幾個額外的紋理座標來實現,例如。如果性能下降,這個部分就是瓶頸。

頂點變換是由頂點着色器或固定功能管線的轉換和照明功能完成的。對於頂點着色器瓶頸, 測試包括使着色器程序更長。爲了確保編譯器沒有優化這些附加指令,必須採取一些注意事項。對於固定功能管線,可以通過打開附加功能(如鏡面高光)或將光源轉換成更復雜的形式(例如聚光燈)來提高處理負荷。

下文將對幾何階段兩個常可能是瓶頸的階段的定位方法進行進一步論述。



3.2.1 頂點與索引傳輸的瓶頸定位


GPU渲染管線的第一步,是讓GPU獲取頂點和索引。而GPU獲取頂點和索引的操作性能取決於頂點和索引的實際位置。其位置通常是在系統內存中(通過AGP或PCI Express總線傳送到GPU),或在局部幀緩衝內存中。通常,在PC平臺上,這取決於設備驅動程序而不是應用程序,而現代圖形API允許應用程序提供使用提示,以幫助驅動程序選擇正確的內存類型。

可以通過調整頂點格式的大小,來確定得到頂點或索引傳輸是否是應用程序的瓶頸。

如果數據放在系統內存內,得到頂點或索引的性能與AGP或PCI Express總線傳輸速率有關;如果數據位於局部緩衝內存,則與內存頻率有關。

如果上述測試對性能都沒有明顯影響,那麼頂點與索引傳輸階段的瓶頸也可能位於CPU上。我們可以通過對CPU降頻來確認這一事實,如果性能按比例進行變化,那麼CPU就是瓶頸所在。


3.2.2 頂點變換的瓶頸定位


渲染管線中的頂點變換階段(Vertex Transformation Stage)負責輸入一組頂點屬性(如模型空間位置、頂點法線、紋理座標等等),以及生產一組適合裁剪和光柵化的屬性(如齊次裁剪空間位置,頂點光照結果,紋理座標等等)。當然,這個階段的性能與每個頂點完成的工作,以及正在處理的頂點數量有關。

對於可編程的頂點變換,只要簡單地改變頂點程序的長度,就能確定頂點處理是否是瓶頸。如果此時發生性能的變化,就可以判定頂點處理階段是瓶頸所在。如上文提到過的,如果要增加指令,在添加富有意義的指令時需要留心,

以防止被編譯器或驅動將指令優化掉。例如,因爲驅動程序通常不知道程序編譯時常量的值,沒有被常量寄存器引用的空操作指令(no-ops)不能被優化(如加入一個含有值爲零的常量寄存器)。

對於固定功能的頂點變換,判定瓶頸則有點麻煩。試着通過改變頂點的工作,例如修改鏡面光照或紋理座標生成的狀態來修改負載。

另外需要注意,頂點處理的速度與GPU核心頻率有關。



3.3 應用程序階段的瓶頸定位


以下是應用程序階段的瓶頸定位的一些策略的總結:

    • 可以用Profiler工具查看CPU的佔用情況。主要是看當前的程序是否使用了接近100%的CPU佔用。比如AMD出品的Code
      Analyst代碼分析工具,可以對運行在CPU上的代碼進行分析和優化。Intel也出品了一個稱爲Vtune的工具,可以分析在應用程序或驅動器(幾何處理階段)中時間花費的位置情況。
  • 一種巧妙的方法是發送一些其他階段工作量極小甚至根本不工作的數據。對於某些API而言,可以通過簡單地使用一個空驅動器(就是指可以接受調用但不執行任何操作)來取代真實驅動器來完成。這就有效地限制了整個程序運行的速度,因爲我們沒有使用圖形硬件,因此CPU始終是瓶頸。通過這個測試,我們可以瞭解在應用階段沒有運行的階段有多大的改進空間。也就是說,請注意,使用空驅動程序還隱藏了由於驅動程序本身和階段之間的通信所造成的瓶頸。
  • 另一個更直接的方法是對CPU 進行降頻( Underclock)。如果性能與CPU速率成正比,則應用程序的瓶頸與CPU相關。但需要注意,降頻的方法可以幫助識別瓶頸,也有可能導致一個之前不是瓶頸的階段成爲瓶頸。
  • 另外,則是排除法,如果GPU階段沒有瓶頸,那麼CPU就一定是瓶頸所在。




四、下篇:渲染管線的優化策略


一旦確定了瓶頸位置,就可以對瓶頸所處階段進行優化,以改善我們遊戲的性能。下面根據解決問題的不同階段,對一些優化策略進行了分類整理,將分爲六個部分來進行呈現:

    • 對CPU的優化策略
    • 應用程序階段的優化策略
    • API調用的優化策略
    • 幾何階段的優化策略
    • 光照計算的優化策略
    • 光柵化階段的優化策略


4.1 對CPU的優化策略


許多應用的瓶頸都位於CPU,有的是正當理由(如複雜的物理或AI運算)導致,有的是因爲不好的批處理或資源管理導致。如果已經發現應用程序受到CPU限制,可以試行下列建議,以對渲染管線中CPU的性能進行優化。


4.1.1 減少資源鎖定


每當執行一個需要訪問GPU的同步操作,就可能嚴重堵塞GPU管線,這將消耗CPU和GPU兩者的週期。CPU必須保持在一個循環中,等待GPU管線工作,直到它閒下來並返回所請求的資源,這種等待會造成CPU週期的浪費。然後GPU等待對管線的再填充,這種等待又造成GPU週期的浪費。

上述的鎖定發生在以下情況下:

    • 對前面正在渲染的表面進行鎖定或讀出時
    • 對GPU正在讀的表面進行寫入,例如紋理或頂點緩衝區

而減少資源鎖定的方法,可以嘗試避免訪問渲染期間GPU正在使用的資源。


4.1.2 批次的尺寸最大化


這個策略也可以稱爲“將批次的數量減到最小”。

批次(batch)是調用單個API渲染所做的一組基本渲染。用來繪製幾何體的每個API函數調用,都有對應的CPU消耗。因此最大限度地增加每次調用所提交的三角形的數量,CPU渲染給定數目三角形的消耗就可以減到最小。也即批次的尺寸乘以批次數量得到的工作總量一定,此消彼長。

使批次最大化的技巧列舉如下:

    • 若使用了三角形帶(Triangle Strips),則使用退化三角形(Degenerate Triangles)將不相交的條帶拼接起來。這樣就能夠一次發送多條三角形帶,以便能在單個Draw Call中共享材質。
    • 使用紋理頁(Texture Pages)。不同物體使用不同紋理時,批次時常會被打破,若通過把多個紋理安排進單個的2D紋理內並適當設定紋理座標,就能在單個Draw Call中發送使用了多個紋理的幾何體。此技術可能存在mipmapping和反走樣的問題,而回避大部分這類問題的技術是,把單個的2D紋理打包到一個立方體貼圖的各個面內。
    • 使用Shader分支來增加單個批次大小從而合批。現代GPU具有靈活的頂點和片元處理管線。允許Shader裏有分支。例如,若兩個分開的批次,因爲其中一個需要四個骨骼蒙皮頂點着色器,而另一個需要兩個骨骼蒙皮頂點着色器,你可以編寫一個頂點着色器來遍歷所需的骨骼數量,累積混合權重,然後在權重相加爲一個時跳出循環。這樣兩個批次就可以合併爲一個。在不支持shader分支的架構上,可以實現相似的功能,方法是上述兩種情況都使用4塊骨骼的頂點着色器,對骨骼數量不足4塊的頂點,將其骨骼權重因子設置爲0。
    • 將頂點着色器常量內存(vertex shader constant memory)作爲矩陣查找表(Lookup Table of matrices)使用。通常,當許多小對象共享所有的屬性,但僅矩陣狀態不同時(例如,含相似樹木的森林,或一個粒子系統),批次就會遭到破壞。這時,可以把n個不同的矩陣加載到頂點着色器常量內存中,並將索引以每個對象的頂點格式存儲在常量內存中。然後使用此索引查詢頂點shader的常量內存,並使用正確的變換矩陣,從而一次渲染n個對象。
    • 儘可能遠地往管線下端推遲決策。若要速度更快,應該使用紋理的alpha通道作爲發光值,而不是打破批次,爲光澤設定一個像素shader常量。同樣地,把着色數據放入紋理和頂點可以使單個批次的提交量更大。


4.2 應用程序階段的優化策略


對應用程序階段的優化,可以通過提高代碼的執行速度,以及提到程序的存儲訪問速度(或者減少存儲訪問的次數)來實現。下面將給出一些通用的優化技術,適用於大多數的CPU。

最基本的代碼優化策略包括爲編譯器打開優化標誌。通常有很多不同的標誌,一般需要檢查哪些標誌可以應用於程序代碼中,而且對所使用的優化選項一般不做任何假設。例如,可以將編譯器的開關設置爲“最小代碼大小(minimize code size)”而不是“速度優化(optimizing for speed)”,這樣可以導致代碼執行速度的提高,因爲緩衝性能提高了。此外,如果可能的話,可以嘗試不同的編譯器,因爲不同編譯器一般是按照不同的方式進行優化的。

對於代碼優化來說,定位大部分時間花在哪部分代碼是很關鍵的。一個好的代碼profiler是找到大部分運行時間都花費在代碼哪裏的關鍵。然後在這些地方進行優化工作。而這些位置通常是內部循環,或是每幀執行多次的代碼片段。(PS:本文文末提供了一系列常用的profiler工具的列表)

優化的基本原則是嘗試多種策略,包括重新檢查算法,假設,以及代碼語法等,也就是儘可能多的嘗試各種變化情況。

下文將從內存層面和代碼層面進一步說明。


4.2.1 內存層面的優化


對於存儲層次結構來說,如何在各種不同的CPU體系結構上編寫執行速度較快的代碼變得越來越重要。在編寫程序時,應該注意下列準則:

    • 在代碼中連續訪問的存儲內容在內存中也應保持連續存儲。例如,當渲染一個三角形網格的時候,如果訪問的順序是:紋理座標#0、法線#0、顏色#0、頂點#0、紋理座標#1、法線#1等,那麼在內存中也應該按這個順序連續存儲。儘量避免指針的間接、跳轉,以及函數調用,因爲它們很容易顯著降低CPU中緩衝的性能。比如當一個指針指向另一個指針,而這個指針又指向其他指針時,以此類推,類似典型的鏈表和樹結構,而這將導致數據緩存未命中(cache misses for data)。爲了避免這種情況,應該儘可能使用數組來代替。

PS: 上述條準則的思想有點類似《Game Programming Patterns》書中講到的數據局部性模式(Data Locality pattern),具體可以參考《Game Programming Patterns》這本書的web版關於數據局部性模式的講解:gameprogrammingpatterns.com

    • 某些系統中,默認的內存分配和刪除功能可能比較慢,因此,在啓動時最好爲相同大小的對象分配一個大的內存池,然後使用自己分配或空閒部分來處理該池的內存。
    • 儘量嘗試去避免在渲染循環中分配或釋放內存。例如,可以單次分配暫存空間(scratch space),並且使用棧、數組等其他僅增長的數據結構(也可以使用標誌位來標識哪些元素應該被視爲已刪除)。
    • 對數據結構嘗試用不同的組織形式。例如,Hecker[2]指出,對於一個簡單的矩陣乘法器而言,通過不同的矩陣結構可以節省大量的計算開銷。例如,一個結構數組如下:
struct Vertex {float x,y,z;} 
Vertex myvertices[1000];

或者爲:

struct VertexChunk {float x[1000],y[1000],z[1000];} 
VertexChunk myvertices;

對於給定的體系結構而言,上述第二種結構對於SIMD命令來說要更好一些。但是隨着頂點數目的增多,高速緩存的命中失誤率也會隨之增多。當數組大小增加到一定程度時,下面這種混合方案可能是最好的一種選擇:

struct Vertex4 {float x[4],y[4],z[4];} 
Vertex4 myvertices[250];


4.2.2 代碼層面的優化


下面的會列出編寫與計算機圖形相關的高效代碼的一些技術。這些方法隨着編譯器和不斷髮展的CPU而有所不同,但大多數已經保存了很多年(主要是針對C/C++而言):

    • 善用SIMD。單指令多數據流(Single Instruction Multiple Data,SIMD),例如Intel的MMX或SSE,以及AMD的3D Now!指令集,在很多情形下能獲得很好的性能,可以並行計算多個單元,且比較適合用於頂點操作。
    • 使用float轉long轉換在奔騰系列處理器上速度較慢。如果可以請儘量避免。
    • 儘可能避免使用除法。相對於其他大多數指令而言,執行除法指令所需要的時間大約是執行其他指令所需時間的2.5倍或更多。
    • 許多數學函數,如sin、cos、tan、exp、arcsin等,計算開銷較高,使用的時候必須小心。如果可以接受低精度,那麼只需要使用麥克勞林(MacLaurin)或泰勒(Taylor)級數的前幾項即可。由於現代CPU對內存的訪問的代價依然很高,因此使用級數的前幾項比使用查找表(Lookup Tables)強得多。
    • 條件分支會有一定的開銷,Shader中的條件分支開銷尤甚。儘管大多數處理器都有分支預測功能,但是這意味着只有準確地進行分支預測,纔有可能降低計算開銷。錯誤的分支預測對一些體系結構、特別是對於具有深管線的體系結構來說,計算開銷通常會較高。
    • 對於經常調用的小函數使用內聯(Inline)。
    • 在合理的情況下減少浮點精度,比如用float代替double。而當選用float型來代替double型數據時,需要在常數末尾加上一個f。否則,整個表達式就會被強制轉換爲double型;因此,語句float x =2.42f;要比float x = 2.42;執行得更快。
    • 儘可能使用低精度數據,讓發送到圖形管線的數據量更少。
    • 虛函數方法、動態轉換、(繼承)構造,以及按值傳遞結構體(passing structs by value)都會對效率造成一定影響。據瞭解,一幀畫面中大約有40%的時間花費在用於模型管理的虛擬繼承層次結構上。Blinn提出了一種技術[3],可以避免計算C++中向量表方面的一部分開銷。


4.3 API調用的優化策略


上文已經提到,批次(batch)是調用單個API渲染所做的一組基本渲染。用來繪製幾何體的每個API函數調用,都有對應的CPU消耗。改進批次過小問題的方法有很多種,且它們都有共同的目標——更少的API調用。以下是一些要點。

    • 一種減少API調用的方法是使用某種形式的實例(Instance)。大多數API都支持在一次調用中擁有一個對象並進行多次繪製。因此,與其爲森林中的每一棵樹單獨調用API,不如使用單次調用來渲染樹模型的許多副本。如下圖。

圖4 植被實例(Vegetation instancing)。所有同樣顏色的物體在一個Draw Call中進行渲染。


PS: 此思想有點類似設計模式中的享元模式(flyweight pattern)。具體可以參考《Game Programming Patterns》這本書的web版關於享元模式的精彩講解:gameprogrammingpatterns.com


    • 進行批處理(batching)。批處理的基本思想是將多個對象合併成一個對象,因此只需要一個API調用便可以渲染整個集合。批處理中的合並可以一次性完成,且緩衝區對靜態對象集合都能每幀進行重用。對於動態對象,可以使用多個網格填充單個緩衝區。但這種基本方法的侷限性是,網格中的所有對象都需要使用一組相同的着色器程序,即相同的材質。
    • 可以用不同的顏色來合併對象,例如,通過用標識符對每個對象的頂點進行標記。着色器程序可以根據此標識符,查找使用什麼顏色來遮擋物體。同樣的想法可以擴展到其他表面屬性。類似地,附於表面的紋理也可以用於標識使用哪種材質。而單獨物體的光照貼圖需合併成紋理圖集(texture atlases)或紋理數組(texture array)。
    • 多人同屏的場景很適合使用實例進行渲染,其中每個角色都擁有獨特的一套外表。而進一步的變化可以添加隨機的膚色和貼花。這種基於實例的方法也可以結合LOD技術進行。如下圖。

圖5 多人同屏場景(crowd scene)。使用實例(instance)來減少Draw Call,也可以結合LOD技術使用,比如對於遠處的模型,使用 impostors進行渲染。


    • 提高性能的另一種方法是通過將具有類似渲染狀態的對象(頂點和像素着色器、紋理、材質、光照、透明度等)分組並將它們按順序渲染,從而最小化狀態更改
    • 改變狀態時,有時需要完全或部分地清理管線。出於這個原因,改變着色器程序或材質參數可能非常昂貴。具有使用共享材質(shared material)的節點可以進行分組,以獲得更好的性能,而用共享紋理(shared texture)繪製多邊形可以減小紋理緩存的抖動。另外,正如上文提到的,一種減少紋理改變的變化的方法便是把一些紋理圖像到一個大的紋理圖集或紋理數組中。
    • 理解對象緩衝(object buffer)在渲染時的分配和存儲方式也同樣重要。對於一個含CPU和GPU的系統,GPU和CPU有各自的內存,而圖形驅動程序通常控制對象所在的位置,它也可以給出存儲在何處是更優的建議。一個常見的類型分類是靜態與動態緩衝區。如果一個物體不形變,或者形變可以完全由着色器程序(如蒙皮)完成,那麼在GPU內存中存儲對象的數據是較爲合適的。而該對象的不變屬性可以通過將其存儲在爲靜態緩衝區中。通過這種方式,不必在渲染的每幀在總線上發送這些不變的數據,從而避免在管線的這一階段出現瓶頸。


4.4 幾何階段的優化策略


幾何階段主要負責變換、光照、裁剪、投影,以及屏幕映射。其中,變換和光照過程比較容易優化,其他幾個部分的優化稍顯困難。以下是一些要點:

    • 變換、光照、裁剪、投影,以及屏幕映射操作可以使用較低精度的數據,以減小開銷。
    • 合理地使用索引和頂點緩衝區可以幫助幾何階段減小計算量。
    • 可以簡化模型來減小整個管線的頂點和繪製圖元的數量,以降低頂點數據傳輸和頂點變換成本。而諸如視錐裁剪和遮擋剔除之類的技術避免了將全部的圖元發送到管線。
    • 可以使用緩存感知(cache-oblivious)佈局算法,其中頂點以某種形式排列,以最大限度地提高緩存重用性,從而節省處理時間。(具體可見RTR3原文 12.4.4節)
    • 同理,爲了節省內存和訪問時間,儘可能在頂點、法線、顏色和其他着色參數上,選擇更低精度的數據格式。有時我們會在half、single、double。float精度之間做選擇,需要注意,除了其中因爲精度更低帶來的速度提升外,有些格式也會因爲是硬件內部使用的原生格式(native format)而更快。
    • 減少內存使用的另一種方法是將頂點數據存儲在壓縮格式中。對此,Deering [4]深入討論了這種技術. Calver [5]提出了各種方案,使用頂點着色器進行解壓。zarge [ 6 ]也指出,數據壓縮也有助於調整頂點格式緩存線。而Purnomo等人[ 7 ]結合簡化方法和頂點的量化技術,使用圖像空間的度量,提出了爲一個給定的目標網格尺寸優化網格的方案。


4.4.1 減少頂點傳輸的開銷


頂點傳遞是瓶頸的可能性很小,但也偶有發生。假如頂點或索引(索引是瓶頸的可能性更小)的傳遞是應用瓶頸,可以試着使用下列各項策略:

    • 在頂點格式中使用儘可能少的位。位數足夠即可,不需要對所有數據都使用浮點格式(例如對顏色)。
    • 在頂點程序中產生可推導的頂點屬性,而不是把他們存儲在輸入頂點格式中。例如,正切線(tangent)、法線(normal)和副法線(binormal)通常不需要都存儲。給出任意兩個,能用Vertex-program簡單叉積推導出第三個。這項技術,即爲用頂點處理速度去換取頂點傳輸速率。
    • 使用16位的索引代替32位的索引。16位索引更容易查找。移動起來更輕量,而且佔用的內存更少。
    • 以相對連續的方式訪問頂點數據。當訪問頂點數據時現代GPU可以進行緩存。因爲在任意內存層次中,引用的空間局部性有助於最大化緩存的命中率,這可以減少對帶寬的要求。


4.4.2 頂點處理的優化


頂點處理是現代GPU的瓶頸可能性很小,但是也偶有發生,這取決於所使用的模式和目標硬件。如果發現頂點處理是瓶頸所在,可以試用如下列舉的各項策略:

    • 對變換和照明(T&L)後的頂點存儲進行優化。現代GPU有一個小的先入先出(FIFO)的緩存,用於存儲最近所轉換的頂點結果:命中這個高速緩衝器可以保存所有的變換和照明,以及所有流水線早先完成的工作。爲了利用這個緩存的優勢,必須使用經過索引的圖元,而且必須對頂點進行排序,以最大化網格上的引用局部性。可以幫助完成這個任務的工具有D3DX和NVTriStrip等。
    • 減少所處理的頂點數。這是能想到的很基本的解決方案。但是使用簡單的層次細節方案,例如一組靜態的LOD,確實有助於減少頂點處理的負擔。
    • 使用頂點處理LOD。在使用層次細節減少所處理的頂點數時,可以試着把層次細節用於頂點計算本身。例如,對遠處的任務沒必要完全做4塊骨骼的蒙皮,或許可以使用更輕量的光照近似。而如果當前材質的shader是多通道的,那麼減少位於遠處低LOD級別的渲染通道數量,也會減少頂點處理的成本。
    • 把每個物體的計算留給CPU去做。每個物體或每幀都改變的計算時常在頂點着色器中進行。例如,將方向光矢量轉換到視點空間的通常在頂點shader中進行,雖然計算結果只是每幀改變一次。
    • 使用正確的座標空間。座標空間的選擇時常影響計算視點程序值所需的指令數。例如,計算頂點光照時,如果頂點法線存儲在物體空間中,而方向光矢量存儲在視圖空間中,就須在頂點shader中轉換其中之一,將兩者轉換到統一空間下。而如果改爲在CPU上對每個物體一次性地把光矢量轉換到物體空間,再進行逐個頂點的轉換就沒有必要了,這樣就節省了GPU頂點處理的運算量。
    • 使用頂點分支來“提前結束”計算。例如,若在頂點着色器中循環多個光源,然後進行法線、[0,1]低動態範圍的光照,你可以判斷飽和度到1,或者遠離光源的頂點,來break掉,避免進一步無用的計算。對於蒙皮階段有一個類似的優化,當權重之和達到1時,停止計算(因此所有後來加權的值是0)。需要注意,這個方法是否生效,依賴於GPU如何實現頂點分支,無法在所有架構上保證性能的改善。


4.5 光照計算的優化策略


考慮光照的影響可以每頂點,每像素的進行計算,光照計算可以通過多種方式進行優化:

    • 首先,應該考慮使用的光源類型,以及可以考慮是否所有的多邊形都需要光照。有時模型只需紋理貼圖,或者在頂點使用紋理,或只需要頂點顏色。那麼很多多邊形就無需進行光照計算。
    • 如果光源是靜態的,且照明對象是幾何體,那麼漫反射光照和環境光可以預先計算並存儲在頂點顏色中。這樣做通常被稱爲烘焙照明(baking lighting)。一個預光照(prelighting)更復雜的形式是使用輻射度(Radiosity)方法預先計算場景中的漫反射全局光照,而這樣的光照可以存儲在頂點顏色或光照貼圖(lightmaps)中。
    • 控制光源的數量。光源的數量影響幾何階段的性能,更多的光源意味着更少的速度。此外,雙面的光照可以比單面光照更爲昂貴。當對光源使用固定功能距離衰減時,根據物體與光源的距離來關閉/打開光源是有較爲有用,且幾乎不會被察覺。而距離足夠大時,可以關掉光源。
    • 一種常見的優化方法是根據光源的距離來進行剔除,只渲染受本地光源影響的對象。
    • 另一種減少工作的方法是禁用光源,取而代之的是使用環境貼圖(environment map)
    • 如果場景擁有大量光源,可以使用延遲着色技術來限制計算量和避免狀態的變化。


4.6 光柵化階段的優化策略


光柵化階段可以以多種方式進行優化。現將主流的優化策略列舉如下:

    • 善用背面裁剪。對封閉(實心)的物體和無法看到背面的物體(例如,房間內牆的背面)來說,應該打開背面裁剪開關。這樣對於封閉的物體來說,可以將需光柵化處理的三角形數量減少近50%。但需要注意的是,雖然背面裁剪可以減少不必要的圖元處理,但需要花費一定的計算量來判斷圖元是否朝向視點。例如,如果所有的多邊形都是正向的,那麼背向裁剪計算就會降低幾何階段的處理速度。
    • 一種光柵化階段的優化技術是在特定時期關閉Z緩衝(Z-buffering)。例如,在清楚幀緩衝之後,必須要進行深度測試也可以直接渲染出任何背景圖像。如果屏幕上的每個像素保證被某些對象覆蓋(如室內場景,或正在使用背景天空圖),則不需要清楚顏色緩衝區。同樣,確保只有在需要時才使用混合模式(blend modes)。
    • 值得一提的是,如果在使用Z緩衝,在一些系統上使用模板緩衝不需要額外的時間開銷。這是因爲8位的模板緩衝的值是存儲爲24位z深度值的同一個word中。
    • 優先使用原生的紋理和像素格式。即使用顯卡內部使用的原生格式,以避免可能會有的從一種格式到另一種格式的昂貴轉換。
    • 另一種適用於光柵化階段的優化技術是進行合適的紋理壓縮。如果在送往圖形硬件之前已經將紋理壓縮好,那麼將它發送到紋理內存中的速度將會非常迅速。壓縮紋理的另一個優點是可以提高緩存使用率,因爲經過壓縮的紋理會使用更少的內存。
    • 另一種有用的相關優化技術是基於物體和觀察者之間的距離,使用不同的像素着色器。例如,在場景中有三個飛碟模型,最接近攝像機的飛碟的可能用詳細的凹凸貼圖來進行渲染,而另外兩個較遠的對象則不需要渲染出細節。此外,對最遠的飛碟可以使用簡化的鏡面高光,或者直接取消高光,來簡化了計算量以及減少採樣次數。
    • 理解光柵化階段的行爲。爲了很好地理解光柵階段的負荷,可以對深度複雜度進行可視化,所謂的深度複雜度就是指一個像素被接觸的次數。生成深度複雜度圖像的一種簡單方法就是,使用一種類似於OpenGL的glBlendFunc(GL ONE,GL ONE)調用,且關閉Z緩衝。首先,將圖像清除成黑色;然後,對場景中所有的物體,均使用顏色(0,0,1)進行渲染。而混合函數(blend function)設置的效果即是對每個渲染的圖元來說,可以將寫入的像素值增加(0,0,1)。那麼,深度複雜度爲0的像素是黑色,而深度複雜度爲255的像素爲全藍色(0, 0, 255)。
    • 可以通過計數得到通過Z緩衝與否的像素的數量,從而確定需進一步優化的地方。使用雙通道的方法對那些通過或沒通過Z緩衝深度測試的像素進行計數。在第一個通道中,激活Z緩衝,並對那些通過深度測試的像素進行計數。而對那些沒有通過深度測試的像素進行計數,可以通過增加模板緩衝的方式。另一種方法是關閉Z緩衝進行渲染來獲得深度複雜度,然後從中減去第一個通道的結果。

通過上述方法得到結果後,可以確認:

(1)場景中深度複雜度的平均值、最小值和最大值

(2)每個圖元的像素數目(假定已知場景中圖元的數目);

(3)通過或沒有通過深度測試的像素數目。

而上述這些像素數量對理解實時圖形應用程序的行爲、確定需要進一步優化處理的位置都非常有用。

通過深度複雜度可以知道每個像素覆蓋的表面數量,重複渲染的像素數量與實際繪製的表面的多少是相關的。假設兩個多邊形覆蓋了一個像素,那麼深度複雜度就是2。如果開始繪製的是遠處的多邊形,那麼近處的多邊形就會重複繪製整個遠處的多邊形,重繪數量也就爲1。如果開始繪製的是近處的多邊形,那麼遠處的多邊形就不會通過深度測試,從而也就沒有重繪問題。假設有一組不透明的多邊形覆蓋了一個像素,那麼平均繪製數量就是調和級數:

H(n)=1+\frac{1}{2}+\frac{1}{3}+...+\frac{1}{n} \\

上式背後所包含的邏輯是:第一個繪製的多邊形是一次繪製:第2個多邊形在第一個多邊形之前繪製的概率是1/2:第三個多邊形在前兩個多邊形前繪製的概率是1/3。依次類推,當n取極極限時:

\lim_{n\rightarrow \infty }H(n) = ln(n)+\gamma \\

其中,γ=0.57721…是Euler-Mascheroni常量。當深度複雜度很低時,重繪量會急劇增加,但增加速度也會逐漸減少。深度複雜度爲4,平均繪製2.08次,深度複雜度爲11,平均繪製3.02次,但深度複雜度爲12367,平均繪製10次。

通過進行粗排序,並從前向後場景的渲染對性能提升會有幫助。這是因爲後面繪製的被遮擋物體無需寫入顏色緩衝區或Z緩衝區中。此外,在到達像素着色器程序之前,像素片元也可以被遮擋剔除硬件丟棄掉。


  • 另一種稱爲“early z pass”的技術對帶複雜片元着色器的表面很有用。即首先渲染z緩衝,然後再對整個場景進行渲染。此方法對於避免不必要的像素着色器的計算非常有用,因爲只有可見的表面纔會進行像素着色的計算。而通過BSP樹遍歷或顯式地排序提供了一個粗略的前後順序,可以提供很多優勢,而不需要額外的Pass。


4.6.1 加速片元着色


如果你正在使用長而複雜的片元着色器,那麼往往瓶頸就處於片元着色器中。若果真如此,那麼可以試試如下這些建議:

    • 優先渲染深度。在渲染主要着色通道(Pass)前,先進行僅含深度的通道(depth-only (no-color) pass)的渲染,能顯著地提高性能,尤其是在高深度複雜性的場景中。因爲這樣可以減少需要執行的片元着色量,以及幀緩衝存儲器的存取量,從而提高性能。而爲了發揮僅含深度的通道的全部優勢,僅僅禁用顏色寫入幀緩衝是遠遠不夠的,同時也應該禁用所有向片元的着色,甚至禁用影響到深度以及顏色的着色(比如 alpha test)。
    • 幫助early-z優化(即Z緩衝優化),來避免多餘片元處理 。現代GPU配有設計良好的芯片,以避免對被遮擋片元的着色,但是這些優化依賴場景知識。而以粗略地從前向後的順序進行渲染,可以明顯提高性能。以及,先在單獨的pass中先渲染深度(見前一條tip),通過將着色深度複雜度減少到1,可以有效地幫助之後的pass(主要的昂貴的shader計算的位置)進行加速。
    • 在紋理中存儲複雜功能。紋理作爲查找表( lookup tables)其實非常好用,而且可以無消耗地過濾它們的結果。一個典型例子便是單位立方體貼圖,它僅允許以一個單一紋理查找的代價來高精度地對任意向量進行標準化。
    • 將更多每片元的工作移到頂點着色器。對於優化的大方向而言,正如頂點着色器中的每個物體的計算量工作應該儘可能地移到CPU中一樣,每頂點的計算也應該儘量被移到頂點着色器(連同在屏幕空間中線性插值計算)。常見的例子包括計算向量和座標系之間的變換向量。
    • 使用必需的最低精度。諸如DirectX之類的API允許我們在着色器代碼中指定精度,以減少精度高所帶來的額外計算量。很多GPU都可以利用這些提示來減少內部精度以及提高性能。
    • 避免過度歸一化(Normalization)。在寫shader時,對每個步驟的每個矢量都進行歸一化的習慣,常常被調侃爲“以歸一化爲樂(Normalization-Happy)”。這個習慣通常來說其實是不太好的習慣。我們應該意識到不改變長度的變換(例如標準正交基上的變換)和不依賴矢量長度的計算(例如正方體貼圖的查詢)是完全沒必要進行歸一化後再進行的。
    • 考慮使用片元着色器的LOD層次細節。雖然片元着色器的層次細節不像頂點着色器的層次細節影響那麼大(由於投射,在遠處物體本身的層次細節自然與像素處理有關),但是減少遠處着色器的複雜性和表面的通道數,可以減少片元處理的負載。
    • 在不必要的地方禁用三線性過濾。在現代GPU結構的片元着色器中計算三線性過濾(Trilinear filtering),即使不消耗額外的紋理帶寬,也要消耗額外的循環。在mip級別轉換不容易辨別的紋理上,關掉三線性過濾,可以節省填充率。
    • 使用儘可能簡單的Shader類型。在Direct3D和OpenGL中,對片元進行着色都有多種方法。舉例來說,在Direct3D 9中,可以指定片元着色的使用,隨着複雜性和功率的增加,有紋理階段、像素shader版本 1.x、像素 shader版本 2.x,以及像素shader 3.0等。一般而言,應該使用最簡單的着色器版本來創建預期的效果。更簡單的着色版本提供了更多的一些隱式編譯選項,通常可以用來讓它們更快地被GPU驅動程序編譯成處理像素的原生代碼。


4.6.2 減少紋理帶寬


如果發現內存帶寬是瓶頸,但是大部分結果又要從紋理中取得,那麼可以考慮以下方面的優化。

    • 減少紋理尺寸。考慮目標分辨率和紋理座標。如果玩家是不是真的會看到最高級別的mip級別,如果不是,就應該考慮縮減紋理大小。此方法在超載的幀緩衝存儲器從非本地存儲器(例如系統存儲器,通過AGP或PCI Express總線)強制進行紋理化時會非常有用。一個NVIDIA在2003年出品的名叫NVPerfHUD的工具可以幫助診斷這個問題,其顯示了各個堆(heaps)中由驅動所分配的內存量。
    • 壓縮所有的彩色紋理。應該壓縮作爲貼花或細節的一切紋理,根據特定紋理alpha的需要,選用DXT1、DXT3或DXT5進行壓縮。這個步驟將會減少內存使用,減少紋理帶寬需求,並提高紋理緩存效率。
    • 避免沒必要的昂貴紋理格式。64位或128位浮點紋理格式,顯然要花費更多帶寬,僅在不得已時纔可以使用它們。
    • 儘可能地在縮小的表面上使用mipmapping。mipmapping除了可以通過減少紋理走樣改善質量外,還可以通過把紋理內存訪問定位在縮小的紋理上來改善紋理緩存效用。如果發現某個mipmapping使表面看起來很模糊,不要禁用mipmapping,或增加大的LOD級別的基準偏移,而是使用各向異性過濾(anisotropic filtering),並適當調整每個批次各向異性的級別。


4.6.3 優化幀緩衝帶寬


管線的最後階段,光柵化操作,與幀緩衝存儲器直接銜接,是消耗幀緩衝帶寬的主要階段。因此如果帶寬出了問題,經常會追蹤到光柵化操作。下面幾條技巧將講到如何優化幀緩衝帶寬。

    • 首先渲染深度。這個步驟不但減少片元着色的開銷(見上文),也會減少幀緩衝帶寬的消耗。
    • 減少alpha混合。當alpha混合的目標混合因子非0時,則要求對幀緩衝區進行讀取和寫入操作,因此可能消耗雙倍的帶寬。所以只有在必要時才進行alpha混合,並且要防止高深度級別的alpha混合複雜性。
    • 儘可能關閉深度寫入。深度寫入會消耗額外的帶寬,應該在多通道的渲染中被禁用(且多通道渲染中的最終深度已經在深度緩衝區中了)。比如在渲染alpha混合效果(例如粒子)時,也比如將物體渲染進陰影映射時,都應該關閉深度寫入。另外,渲染進基於顏色的陰影映射也可以關閉深度讀取。
    • 避免無關的顏色緩衝區清除。如果每個像素在緩衝區都要被重寫,那麼就不必清除顏色緩衝區,因爲清除顏色緩衝區的操作會消耗昂貴的帶寬。但是,只要是可能就應該清除深度和模板緩衝區,這是因爲許多早期z值優化都依賴被清空的深度緩衝區的內容。
    • 默認大致上從前向後進行渲染。除了上文提到的片元着色器會從默認大致上從前向後進行渲染這個方法中受益外,幀緩衝區帶寬也會得到類似的好處。早期z值硬件優化能去掉無關的幀緩衝區讀出和寫入。實際上,沒有優化功能的老硬件也會從此方法中受益。因爲通不過深度測試的片元越多,需要寫入幀緩衝區的顏色和深度就越少。
    • 優化天空盒的渲染。天空盒經常是幀緩衝帶寬的瓶頸,因此必須決定如何對其進行優化,以下有兩種策略:

(1)最後渲染天空盒,讀取深度,但不寫入深度,而且允許和一般的深度緩衝一起進行早期early-z優化,以節省帶寬。

(2)首先渲染天空盒,而且禁用所有深度讀取和寫入。

以上兩種策略,究竟哪一種會節省更多開端,取決於目標硬件的功能和在最終幀中有多大部分的天空盒可見。如果大部分的天空盒被遮擋,那麼策略(1)更好,否則,策略(2)可以節省更多帶寬。

    • 僅在必要時使用浮點幀緩衝區。顯然,這種格式比起較小的整數格式來說,會消耗更多的帶寬,所以,能不用就不用。對多渲染目標( Multiple Render Targets,MRT)也同樣如此。
    • 儘可能使用16爲的深度緩衝區。深度處理會消耗大量帶寬,因此使用16位代替32位是極有好處的,且16位對於小規模、不需要模板操作的室內場景往往就足夠了。對於需要深度的紋理效果,16位深度緩衝區也常常足夠渲染,如動態的立方體貼圖。
    • 儘可能使用16位的顏色。這個建議尤其適用於對紋理的渲染效果,因爲這些工作的大多數,用16位的顏色能工作得很好,例如動態立方體貼圖和彩色投射陰影貼圖。


綜上,現代GPU能力和可編程性的增強,使得改善機器性能變得更復雜。無論是打算加速應用程序的性能,還是希望無成本地改善圖像質量,都需要對渲染管線的內部工作原理有深刻理解。而GPU管線優化的基本思路是,通過改變每個單位的負荷或計算能力來識別瓶頸,然後運用每個傳遞單元工作原理的理解,系統地解決那些瓶頸。


五、主流性能分析工具列舉


有很多不錯的分析圖形加速器和CPU使用的的工具,以及性能優化相關的Profiling工具,在這裏,將主流的工具進行列舉:


現今主流遊戲引擎提供的Profiler有:


圖6 Unreal Engine的GPU Visualizer


圖7 Unity的Profiler



六、更多性能優化相關資料


    • 雖然有點過時,Cebenoyan的文章[1]概述瞭如何找到提高效率的瓶頸和技術。
    • 《NVIDIA's extensive guide》[8]包含了相關的各種主題。
    • 一些很讚的C++優化指南包括Fog的文章[9]和Isensee的文章[10]。


Reference


[1] Cebenoyan, Cem, “Graphics Pipeline Performance,” in Randima Fernando, ed.,GPU Gems, Addison-Wesley, pp. 473–486,2004. Cited on p. 681, 699, 701, 716,722

[2] Hecker, Chris, “More Compiler Results, and What To Do About It,” Game Developer, pp. 14–21, August/September 1996. Cited on p. 705

[3] Blinn, Jim, “Optimizing C++ Vector Expressions,” IEEE Computer Graphics &Applications, vol. 20, no. 4, pp. 97–103, 2000. Also collected in [110], Chapter 18.Cited on p. 707

[4] Deering, Michael, “Geometry Compression,” Computer Graphics (SIGGRAPH 95 Proceedings), pp. 13–20, August 1995. Cited on p. 555, 713

[5] Calver, Dean, “Vertex Decompression Using Vertex Shaders,” in Wolfgang Engel, ed., ShaderX, Wordware, pp. 172–187, May 2002. Cited on p. 713

[6] Zarge, Jonathan, and Richard Huddy, “Squeezing Performance out of your Game with ATI Developer Performance Tools and Optimization Techniques,”Game Developers Conference, March 2006. ati.amd.com/developer/g Session-Zarge-PerfTools.pdf

Cited on p. 270, 699, 700, 701,702, 712, 713, 722, 847

[7] Purnomo, Budirijanto, Jonathan Bilodeau, Jonathan D. Cohen, and Subodh Kumar,“Hardware-Compatible Vertex Compression Using Quantization and Simplification,”Graphics Hardware, pp. 53–61, 2005. Cited on p. 713

[8] NVIDIA Corporation, “NVIDIA GPU Programming Guide,” NVIDIA developer website, 2005. http://developer.nvidia.com/object/gpu programming guide.htmlCited on p. 38, 282, 699, 700, 701, 702, 712, 722

[9] Fog, Agner, Optimizing software in C++, 2007. Cited on p. 706, 722

[10] Isensee, Pete, “C++ Optimization Strategies and Techniques,” 2007. Cited on p.706, 722


The end.

再次,感謝大家一直以來的支持與陪伴。

With best wish.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章