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.

一般最佳化技巧

前言

在理想世界中,電腦能以無限的速度運作,唯一的限制就是我們的想像力。然而在現實中,要寫出讓即使是最快電腦也跑不動的軟體,其實非常容易。

因此,設計遊戲或其他軟體時,我們必須在「想做的事情」和「在維持良好效能下實際能做到的事情」之間取得平衡。

要達到最佳結果,我們有兩種方法:

  • 讓運作更快。

  • 讓方法更聰明。

理想上,兩者應該兼顧。

障眼法與假象

聰明地開發的一環,就是知道在遊戲裡,我們經常能讓玩家相信他們正處在一個比實際更複雜、更互動、畫面更華麗的世界。一個好的程式設計師就像魔術師,應該不斷學習業界的技巧,並試著發明新手法。

效能瓶頸的本質

對外行人來說,所有效能問題看起來都一樣,但其實效能問題有不同類型:

  • 每一幀都會發生的慢處理,導致持續低幀率。

  • 間歇性發生、造成效能「尖峰」導致遊戲卡頓的處理。

  • 在一般遊戲流程外才出現的慢處理,比如載入關卡時。

這些問題都會讓玩家感到困擾,只是方式不同。

效能量測

進行效能優化時,最重要的工具就是能夠量測效能:也就是能夠找出瓶頸在哪裡,並評估我們的優化措施是否有效。

量測效能有幾種常見方法,包括:

要特別注意,不同的硬體設備上,不同區塊的效能表現可能差異很大。建議在多個裝置上量測效能,若目標平台包含行動裝置尤其要注意這點。

限制

CPU 分析器通常是效能分析的首選工具,但它們不一定能呈現全部事實。

  • 瓶頸也常常會在 GPU,那是 CPU 發出指令後的「結果」。

  • 有些效能尖峰,其實是在作業系統層級(Godot 之外)發生,是 Godot 執行某些操作(例如動態記憶體配置)後的「結果」。

  • 由於初始設定繁複,你不一定能隨時在特定裝置(例如手機)做效能分析。

  • 有時你甚至得解決那些你手邊沒有的硬體上發生的效能問題。

因此,你往往需要靠「偵探式」的排查手法來找出瓶頸所在。

偵探式排查

偵探式排查對開發者而言非常重要,不只用在效能問題,也常用於除錯。這包括假設驗證和二分搜尋法。

假設驗證

假設你懷疑遊戲慢是因為精靈(Sprite)太多,可以這麼驗證:

  • 嘗試增加或減少精靈數量,觀察效能變化。

這又引發新假設:是否精靈圖片尺寸越大,效能掉越多?

  • 那就只改變精靈尺寸,保持其他條件不變,然後量測效能來驗證。

效能分析工具

效能分析工具(Profiler)可以在程式執行時記錄各區塊的耗時,並統計每個函式或模組佔用的時間百分比,以及函式被呼叫的次數。

這對找出瓶頸、量測改善效果都很有幫助。有時候優化反而讓效能變差,所以務必要用效能分析和計時來指引你的優化方向。

有關使用 Godot 內建分析器的更多資訊,請參考 效能分析器

原則

Donald Knuth 說:

程式設計師花了大量時間去思考或擔心程式中非關鍵部分的效能,但這些效率上的嘗試,事實上在除錯和維護時反而帶來負面影響。我們應該忘掉那些微小的效能提升,也就是約 97% 的情況:過早的最佳化是萬惡之源。但對於那關鍵的 3%,我們不能錯過機會。

這些訊息非常重要:

  • 開發時間有限,與其盲目優化所有環節,不如將心力集中在真正重要的部分。

  • 許多優化反而讓程式碼難以閱讀和除錯。我們應該只在真正有顯著效益的地方進行這些犧牲。

不是因為我們 優化某段程式碼,就一定 應該 去優化。知道什麼時候該優化、什麼時候不該動,這是一項很重要的能力。

這段話常被誤解,人們容易只記得「過早的最佳化是萬惡之源」這句。雖然 過早 的最佳化確實不可取,但高效能軟體其實來自於一開始就有高效能的設計。

高效能設計

如果一直到很後面才考慮效能,最大風險就是錯過了設計階段。實際上,最重要的效能決定早在還沒寫第一行程式碼前就已經發生。如果設計和演算法本身就低效,後面再怎麼調整細節,頂多只能「變快一點」,永遠追不上原本設計就為效能考量的程式。

這點在遊戲或圖形程式設計中特別重要。一個設計得當的系統,即使沒做底層最佳化,也常常比設計平庸但拼命微調底層的系統快上好幾倍。

漸進式設計

當然,實務上除非你本來就很有經驗,很難第一次就設計出最理想的架構。通常都會針對某個問題嘗試許多不同方法,做出多個版本,最後才找到滿意的解決方案。在這個階段,別太早花太多時間雕琢細節,等整體設計底定後再來優化細節,不然前面做的多半都會被捨棄。

高效能設計沒有放諸四海皆準的準則,因為每個問題本質不同。不過有個 CPU 方面的原則很值得注意:現代 CPU 幾乎都被記憶體頻寬限制住。因此,資料導向(Data-Oriented)的設計重新受到重視,設計資料結構與演算法時要盡量讓資料存取具有 快取區域性 (cache locality)和線性存取,而非隨機跳躍於記憶體。

最佳化流程

假設我們已經有合理的設計,並記住 Knuth 的建議,最佳化的第一步就是找出最大的瓶頸:那些最慢的函式,也就是「低垂的果實」。

當我們成功改善了最慢的部分後,它可能就不再是瓶頸,這時就要再重新測試/分析,找出下個最值得優化的區塊。

流程如下:

  1. 分析/找出瓶頸。

  2. 優化瓶頸。

  3. 回到步驟 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。