Attention: Here be dragons
This is the latest
(unstable) version of this documentation, which may document features
not available in or compatible with released stable versions of Godot.
Checking the stable version of the documentation...
一般最佳化技巧
前言
在理想世界中,電腦能以無限的速度運作,唯一的限制就是我們的想像力。然而在現實中,要寫出讓即使是最快電腦也跑不動的軟體,其實非常容易。
因此,設計遊戲或其他軟體時,我們必須在「想做的事情」和「在維持良好效能下實際能做到的事情」之間取得平衡。
要達到最佳結果,我們有兩種方法:
讓運作更快。
讓方法更聰明。
理想上,兩者應該兼顧。
障眼法與假象
聰明地開發的一環,就是知道在遊戲裡,我們經常能讓玩家相信他們正處在一個比實際更複雜、更互動、畫面更華麗的世界。一個好的程式設計師就像魔術師,應該不斷學習業界的技巧,並試著發明新手法。
效能瓶頸的本質
對外行人來說,所有效能問題看起來都一樣,但其實效能問題有不同類型:
每一幀都會發生的慢處理,導致持續低幀率。
間歇性發生、造成效能「尖峰」導致遊戲卡頓的處理。
在一般遊戲流程外才出現的慢處理,比如載入關卡時。
這些問題都會讓玩家感到困擾,只是方式不同。
效能量測
進行效能優化時,最重要的工具就是能夠量測效能:也就是能夠找出瓶頸在哪裡,並評估我們的優化措施是否有效。
量測效能有幾種常見方法,包括:
針對想觀察的程式碼區塊加上啟動/停止計時器。
使用 Godot 分析器。
使用 外部 CPU 分析工具。
使用外部 GPU 剖析器/偵錯工具, 例如 NVIDIA Nsight Graphics 、 Radeon GPU Profiler 、 PIX (僅限 Direct3D 12), Xcode (僅限 Metal), 或 Arm Performance Studio 。
檢查影格率 (請關閉 V-Sync)。也可使用第三方工具, 例如 RivaTuner Statistics Server (Windows), Special K (Windows), 或 MangoHud (Linux)。
使用非官方的 除錯選單擴充外掛。
要特別注意,不同的硬體設備上,不同區塊的效能表現可能差異很大。建議在多個裝置上量測效能,若目標平台包含行動裝置尤其要注意這點。
限制
CPU 分析器通常是效能分析的首選工具,但它們不一定能呈現全部事實。
瓶頸也常常會在 GPU,那是 CPU 發出指令後的「結果」。
有些效能尖峰,其實是在作業系統層級(Godot 之外)發生,是 Godot 執行某些操作(例如動態記憶體配置)後的「結果」。
由於初始設定繁複,你不一定能隨時在特定裝置(例如手機)做效能分析。
有時你甚至得解決那些你手邊沒有的硬體上發生的效能問題。
因此,你往往需要靠「偵探式」的排查手法來找出瓶頸所在。
偵探式排查
偵探式排查對開發者而言非常重要,不只用在效能問題,也常用於除錯。這包括假設驗證和二分搜尋法。
假設驗證
假設你懷疑遊戲慢是因為精靈(Sprite)太多,可以這麼驗證:
嘗試增加或減少精靈數量,觀察效能變化。
這又引發新假設:是否精靈圖片尺寸越大,效能掉越多?
那就只改變精靈尺寸,保持其他條件不變,然後量測效能來驗證。
二分搜尋法
如果你知道每幀花的時間明顯超過預期,但不確定是哪裡出問題,可以先把一半的主要處理程式碼註解掉,然後觀察效能提升幅度是否如預期?
一旦知道是哪一半有瓶頸,再繼續對該部分重複此步驟,直到確定是哪裡出了問題。
效能分析工具
效能分析工具(Profiler)可以在程式執行時記錄各區塊的耗時,並統計每個函式或模組佔用的時間百分比,以及函式被呼叫的次數。
這對找出瓶頸、量測改善效果都很有幫助。有時候優化反而讓效能變差,所以務必要用效能分析和計時來指引你的優化方向。
有關使用 Godot 內建分析器的更多資訊,請參考 效能分析器。
原則
Donald Knuth 說:
程式設計師花了大量時間去思考或擔心程式中非關鍵部分的效能,但這些效率上的嘗試,事實上在除錯和維護時反而帶來負面影響。我們應該忘掉那些微小的效能提升,也就是約 97% 的情況:過早的最佳化是萬惡之源。但對於那關鍵的 3%,我們不能錯過機會。
這些訊息非常重要:
開發時間有限,與其盲目優化所有環節,不如將心力集中在真正重要的部分。
許多優化反而讓程式碼難以閱讀和除錯。我們應該只在真正有顯著效益的地方進行這些犧牲。
不是因為我們 能 優化某段程式碼,就一定 應該 去優化。知道什麼時候該優化、什麼時候不該動,這是一項很重要的能力。
這段話常被誤解,人們容易只記得「過早的最佳化是萬惡之源」這句。雖然 過早 的最佳化確實不可取,但高效能軟體其實來自於一開始就有高效能的設計。
高效能設計
如果一直到很後面才考慮效能,最大風險就是錯過了設計階段。實際上,最重要的效能決定早在還沒寫第一行程式碼前就已經發生。如果設計和演算法本身就低效,後面再怎麼調整細節,頂多只能「變快一點」,永遠追不上原本設計就為效能考量的程式。
這點在遊戲或圖形程式設計中特別重要。一個設計得當的系統,即使沒做底層最佳化,也常常比設計平庸但拼命微調底層的系統快上好幾倍。
漸進式設計
當然,實務上除非你本來就很有經驗,很難第一次就設計出最理想的架構。通常都會針對某個問題嘗試許多不同方法,做出多個版本,最後才找到滿意的解決方案。在這個階段,別太早花太多時間雕琢細節,等整體設計底定後再來優化細節,不然前面做的多半都會被捨棄。
高效能設計沒有放諸四海皆準的準則,因為每個問題本質不同。不過有個 CPU 方面的原則很值得注意:現代 CPU 幾乎都被記憶體頻寬限制住。因此,資料導向(Data-Oriented)的設計重新受到重視,設計資料結構與演算法時要盡量讓資料存取具有 快取區域性 (cache locality)和線性存取,而非隨機跳躍於記憶體。
最佳化流程
假設我們已經有合理的設計,並記住 Knuth 的建議,最佳化的第一步就是找出最大的瓶頸:那些最慢的函式,也就是「低垂的果實」。
當我們成功改善了最慢的部分後,它可能就不再是瓶頸,這時就要再重新測試/分析,找出下個最值得優化的區塊。
流程如下:
分析/找出瓶頸。
優化瓶頸。
回到步驟 1。
瓶頸最佳化
有些分析工具甚至能指出是哪個函式的哪個部分(哪個資料存取、哪個運算)拖慢速度。
和設計階段一樣,優化時首先要確保演算法和資料結構已是最佳選擇。資料存取應盡量區域化(善用 CPU 快取),且資料儲存應盡量精簡(同樣記得用分析工具驗證)。許多複雜運算可以事先預先計算好,例如在載入關卡時處理、或直接載入已經預先算好資料的檔案,又或者把運算結果寫成腳本常數直接讀取。
當演算法和資料結構最佳化後,還可以針對細部流程做些小調整來提升效能,例如把某些計算移到迴圈外,或將巢狀 for 迴圈改寫為非巢狀(前提是你已知 2D 陣列的寬高)
每做一項更動,記得都要重新測試瓶頸和耗時。有些改動會讓速度變快,有些則讓效能變差。有時效能提升很小,但程式碼變得很難維護,這種情況下也可以選擇不做這類優化。
附錄
瓶頸數學
俗話說:「一條鍊子的強度取決於最薄弱的環節」,這句話用在效能優化再貼切不過。如果你的專案有 90% 的執行時間都花在函式 A,那麼只要優化 A,就能有極大效益。
A: 9 ms
Everything else: 1 ms
Total frame time: 10 ms
A: 1 ms
Everything else: 1ms
Total frame time: 2 ms
在這個例子中,若將瓶頸 A 提升 9 倍效能,總體幀時間會縮短 5 倍,每秒幀數也會提升 5 倍。
不過,如果還有其他地方也很慢、同樣成為瓶頸時,相同的優化幅度就無法帶來這麼明顯的提升:
A: 9 ms
Everything else: 50 ms
Total frame time: 59 ms
A: 1 ms
Everything else: 50 ms
Total frame time: 51 ms
在這個例子中,雖然我們大幅優化了函式 A,但實際上幀率提升卻很有限。
在遊戲開發中,情況更複雜,因為 CPU 和 GPU 是獨立運作的。你的總幀時間取決於兩者裡面較慢的那一方。
CPU: 9 ms
GPU: 50 ms
Total frame time: 50 ms
CPU: 1 ms
GPU: 50 ms
Total frame time: 50 ms
在這個例子裡,我們大幅優化了 CPU,但幀時間卻沒變快,因為這次的瓶頸是在 GPU。