Garbage Collection in Unreal Engine 4
Garbage Collection (GC) 是 Unreal Engine 4 (UE4) 處理記憶體的方法之一。GC會自動刪掉那些遊戲中已經“不再需要”的物件,“不再需要”的意思就是這個物件已經沒有被其他物件參考到,而 UE4 的 GC 是以 Tracing 去找出些不需要的物件。
1. UE4 中的 GC 會處理哪些物件?
GC 對象就是那些 UObject!
除了自訂的 UClass/UProperty/UFunction 之外,Blueprint 中串邏輯的 node 也都各是一個 UObject,意謂著也受到 GC 管理。
以我們內部的專案來看,光是空的地圖有 40,000 個,而遊戲正式的地圖大概則有 80,000 到 100,000 個。目前知道有 2 個方法可以知道這些 UObject 的詳細資訊:
- 原生 Console command: Obj list
主要是 runtime 時執行來確認 UObject 的數量,結果如下圖。
2. Blueprint Stats Plugin
在開啟 plugin 後,Console command輸入 DumpBlueprintStats。這個指令則是應該在 EditTime 執行,它會列出你專案內的 BP 中使用了哪些 K2Node,多少個 Custom Event 之類的資訊。
2. GC 的成本是?
GC Cost = Search cost + Delete cost
UE4 的 GC 可以設定作用頻率,預設是 60 秒一次,而執行 GC 的那個 frame就會有執行的 GC Cost。
GC 執行時,需要先標記那些不可達的 UObject,然後再 delete 它們。
- Search cost = Mark phase 和 Reachability Analysis
Search 的成本是目前主要的負擔,雖然有 multi thread,但是 UObject 的數量太多時,遊戲就會出現明顯的卡頓。 - Delete cost = 切斷 Reference 與執行 Object Destroy(可以分攤到不同frame,並非主要負擔)。
3. 那要如何最佳化 GC?或減少 GC 時的卡頓?
- 減少 BP Macro 的使用,盡量用 Function
- BP 轉成用 C++
- UE4 提供了開發者可以設定的 GC 最佳化參數
最佳化重點:減少 Search成本,怎樣可以找東西比較快。
- 減少要找的對象總量 — Cluster
- 跳過那些不用 GC 的對象 — Disregard Index
3.1 Project Setting and Log
預設很多功能是關閉的。但既然都要優化了,第一步就是全部都打開!
接著先 packaged 看看結果如何,可能會遇到 Assertion failed 導致遊戲開不起來。如果沒問題,就把 log 開起來看。
# define UE_GCCLUSTER_VERBOSE_LOGGING (1 && !UE_BUILD_SHIPPING)
# define PROFILE_GCConditionalBeginDestroy 1
# define PROFILE_GCConditionalBeginDestroy_byClass 1
先把這三個 defintion 開起來,可以看到比較多資訊。另外,特別注意以下的 4 種 log:
- LogUObjectArray
- LogUObjectAllocator
- LogObj
- LogGarbage
3.2 Cluster
Cluster 是以 Actor 為單位,每個 Actor 會有一個 CanBeInCluster 的變數,而Cluster 必須在 CookedData 才會運作。在做可達性檢查的時候,那些在Cluster 中但不是 Cluster Root 的 object 可以略過,因此可以加快總時間。
最有效的是種在地圖上的 StaticMesh 轉換成 InstancedMeshActor 也可以成為 Cluster。
- 失敗案例:Cluster 失敗的 log,會告訴你失敗的原因。
LogGarbage: Warning: Object BP_HPItemSpawner_C /Game/Map/Area_F/Area_F_Gameplay.Area_F_Gameplay:PersistentLevel.BP_HPItemSpawner2 from cluster LevelActorContainer
/Game/Map/Area_F/Area_F_Gameplay.Area_F_Gameplay:PersistentLevel.ActorCluster is referencing MaterialInstanceDynamic
/Game/Map/Area_F/Area_F_Gameplay.Area_F_Gameplay:PersistentLevel.BP_HPItemSpawner2.ProgressDecal.MaterialInstanceDynamic_0 which is not part of root set or cluster.
- 成功案例:Cluster 建立成功的 log,會告訴你 Cluster size。
LogLevelActorContainer: Created LevelActorCluster (2) for /Game/Map/Area_F/Area_F_Environment.Area_F_Environment:PersistentLevel with 4998 objects, 0 referenced clusters and 104 mutable objects.LogObj: Display: LevelActorContainer /Game/Map/Area_F/Area_F_Environment.Area_F_Environment:PersistentLevel.ActorCluster (Index: 99720), Size 4998, ReferencedClusters: 0
3.3 Disregard Index
Disregard Index是更有效的一手,以我們的遊戲來說,才剛執行起來就有49,500 個 UObject,這些很多是 UE4 自己的 UObject,所以 GC 的時候可以略過他們。
至於這個數字是多少,UE4 有準備一個輔助設定的方法:
根據 Epic 官方文件,只要填入了上面的設定,然後 Package 遊戲,在遊戲啟動時,就會出現 log,便能可以數字填回去。
Server 與 Client 應該要填不同數字,但目前的 Project Setting 卻只有一個欄位可填入…..
4. 最佳化後可以加快幾倍?
以我們的案例來說:
- Client 端可以加快約 11 倍
- Server 端可以加快約 7 倍
有一項 GC Verify 的時間消耗,但那個在 Shipping game 中不會執行