
2、UE4GCプロセス
エントリは、UObjectGlobals.hで定義されているCollectGarbage()関数であり、次のようになります。
void CollectGarbage(EObjectFlags KeepFlags, bool bPerformFullPurge)
{
// No other thread may be performing UObject operations while we're running
AcquireGCLock();
// Perform actual garbage collection
CollectGarbageInternal(KeepFlags, bPerformFullPurge);
// Other threads are free to use UObjects
ReleaseGCLock();
}
このプロセスは、GCロックの取得、CollectGarbageInternalの実行、およびGCロックの解放の3つの部分で構成されています。
2.1GCロックの取得
GCはマルチスレッドであるため、GCロックを設定する必要があります。他のスレッドでUObject関連の操作が実行されると、GCと競合します。これを防ぐために、GCロックが設定されています。この設定は主に非同期ロードプロセスを保護するために使用されます。
1つの機能は、オブジェクトがロードされると、格納された変数には参照を追加する時間がなく、到達不能なガベージコレクションとして扱われることを防ぐということです。次のコードで、FGCScopeGuardはGC操作を防ぐ役割を果たします。
2.1.1FGCSyncObject
GCロックは広い概念です。実際、これはFGCSyncObjectのシングルトンクラスであり、ロックと同期のために複数の変数をカプセル化します。GCの実行中に他のnon-gameスレッドをブロックするために使用することも、non-gameスレッドが重要な操作を実行する時にGCスレッドをブロックするために使用することもできます。もちろん、すべてのケースがブロックされるわけではありません。GCロックをすぐに取得できない場合、各スレッドは特定のロジックに従って他のことを実行することもできます。
主なメンバー変数は次のとおりです。
FThreadSafeCounterAsyncCounter:スレッドセーフカウンターです。スレッドがキーAsync操作を実行する時に、この値を増減させることに使用されています。
FThreadSafeCounter GCCounter:GCロックとして使用されます。0でない場合は、スレッドがGCロックを取得し、GCの実行中であることを意味します。
GCFThreadSafeCounter GCWantsToRunCounter:このカウンターは、スレッドがGCを実行しようとしているが、GCロックがまだ取得されていないことを示します。Asyncスレッドは自動・強制というロジックがありません。この変数へのサポートを手動で実装する必要があります。FCriticalSectionCritical:スレッドがGC関連の操作を実行する重要エリアを保護し、他のユーザーの入りを防ぎます。
FEvent *GCUnlockedEvent:スレッドのsignalと類似し、non-gameスレッドにGCが実行中のevent、実行できるWait()、Trigger()を知らせます。
基本的にGCロックを理解した後、GCロックの取得プロセスを紹介しましょう:
2.2CollectGarbageInternalを実行する
CollectGarbageInternalを実行し、ガベージコレクションを実行し、マークを付けてパージします。
この関数は、KeepFlags、bPerformFullPurgeの2つのパラメーターを受け入れます。KeepFlagsは、このようにマックされたobjectが、参照されているかどうかに関係なく保留されることを意味します。bPerformFullPurgeは、フレームごとに段階的にパージするのではなく、マーキング後に完全なパージを実行するかどうかを示します。
実行プロセスは次のとおりです。
いくつかの注意点があります。
(1)このプロセスはスキャンオブジェクトの到達可能性操作を確実に実行します。黄色の四角い枠の部分は特定のプロセスを示し、後で分析します。状況に応じてパージ操作を行いますが、必要がある限りオブジェクトのパージを行い、しかも完全にパージします。それ以外の場合は、World tickでインクリメンタルパージを行います。
(2)UE4はマルチスレッド環境で実行され、GCはすべてのUObjectで操作するため、ロックの使用に注意してください。
(3)GC自体をマルチスレッド化して、マーキングプロセスを高速化できます。
2.2.1プロセスをマークする
FRealtimeGCのPerformReachabilityAnalysisメソッドを使用して、uobject到達可能性分析を実行します。FRealTimeGCはFGarbageCollectionTracerを継承します。マルチスレッドおよびリアルタイムでオブジェクト参照関係を分析できます。
キーコードは次のとおりです。
// Make sure GC referencer object is checked for references to other objects even if it resides in permanent object pool
if (FPlatformProperties::RequiresCookedData() && FGCObject::GGCObjectReferencer && GUObjectArray.IsDisregardForGC(FGCObject::GGCObjectReferencer))
{
ObjectsToSerialize.Add(FGCObject::GGCObjectReferencer);
}
{
const double StartTime = FPlatformTime::Seconds();
MarkObjectsAsUnreachable(ObjectsToSerialize, KeepFlags, bForceSingleThreaded);
UE_LOG(LogGarbage, Verbose, TEXT("%f ms for Mark Phase (%d Objects To Serialize"), (FPlatformTime::Seconds() - StartTime) * 1000, ObjectsToSerialize.Num());
}
{
const double StartTime = FPlatformTime::Seconds();
PerformReachabilityAnalysisOnObjects(ArrayStruct, bForceSingleThreaded);
UE_LOG(LogGarbage, Verbose, TEXT("%f ms for Reachability Analysis"), (FPlatformTime::Seconds() - StartTime) * 1000);
}
ここでは、FGCArrayStructタイプのデータ構造ArrayStructを使用して、シリアル化ためのuobjectのarrayとweak referenceを格納します。
第一歩として、FGCObject :: GGCObjectReferencerをObjectsToSerializeに追加できます。
FGCObject :: GGCObjectReferencerは、非UObjectオブジェクトでAddReferencedObjectsメソッドを呼び出すために使用できる静的UGCObjectReferencerです。
第二歩は、MarkObjectsAsUnreachableメソッドを呼び出して、KeepFlagsおよびEInternalObjectFlags :: GarbageCollectionKeepFlagsのないすべてのオブジェクトを到達不能としてマークすることです。
まず、GUObjectArrayという変数は、ObjObjects配列がすべてのUObject(FUObjectItemを通してカプセル化します)を格納するグローバルUobject allocatorです。UObjectBase :: InternalIndexプロパティは、配列内でのオブジェクトが対応するFUObjectItemの添え字であるため、添え字に従ってUObjectを検索するか、UObjectを介して対応する添え字を検索すると便利です。
GCに含まれていないobjectは、GUObjectArrayの前部に格納されるため、前のobjectはスキャンされたobjectリストから削除され、後者のobjectのみを考慮し、MaxNumberOfObjectsが取得られます。GCで考慮されない特定のオブジェクトについては、FUObjectArrayの実装を確認できます。
次に、これらのuobjectを到達不能としてマークする必要があります。ここでは、マルチスレッドバージョンのForループが使用されています。マルチスレッド実行の原理は複雑ではありません。まず、現在利用可能なワーカースレッドを取得し、次にマークするobjectをこれらのスレッドに均等に分散してトラバーサルします。マルチスレッドの最下層では、UEのGraphTaskフレームワークを使用します。uobjectをマークする場合、通常の状況で対応するFUObjectItemのプロパティが読み取られ、特別な場合にのみuobjectが読み取られます。FUObjectItemは構造体であり、GUObjectArrayに密接に配置されているため、シーケンシャルトラバーサルでキャッシュに適しています。
UEはクラスター(Cluster)を使用して効率を改善し、その改善方法を以下に紹介します。objectがRootSetに属している場合は、ObjectsToSerializeListに直接追加します。objectがClusterRootまたはClusterに属している場合は、KeepClusterRefsListリストにも追加します。ObjectのClusterRootIndex<=0(ClusterまたはClusterRootにない)の場合は、KeepFlagsがあるかどうかに応じて、到達不能としてマークするかどうかを判断します。マークしない場合は、objectをObjectsToSerializeListに追加し、さらにClusterRootの場合はKeepClusterRefsListに追加します。マークする場合は、objectをClustersToDissolveListに追加し、しかもObjectItemにUnreachableマークを設定します。いくつかの追加処理がClusterで実行されます。詳しくは、コードを参照してください。
第三歩は、PerformReachabilityAnalysisOnObjectsを呼び出して、uobjectの到達可能性を判別します。
ここでは、FGCReferenceProcessor、TFastReferenceCollector、およびFGCCollectorクラスが使用され、これらはすべてシングルスレッドとマルチスレッドの両方をサポートします。
まずReferenceTokenの概念を紹介します。
UObjectシステムでは、各クラスにクラスのリフレクション情報を記述するためのUClassインスタンスがあります。UPropertyを使用して各クラスのメンバー変数を記述することができますが、GCではUPropertyを直接トラバースする場合オブジェクト参照関係をスキャンすると、効率が低下します(オブジェクト以外の参照プロパティが多数あるため)。そのため、UEはReferenceTokenを作成しました。これは、クラス内のオブジェクトへの参照を記述する1セットのtokeのストリームです。次の図に、参照のタイプを示します。
/**
* Enum of different supported reference type tokens.
*/
enum EGCReferenceType
{
GCRT_None = 0,
GCRT_Object,
GCRT_PersistentObject,
GCRT_ArrayObject,
GCRT_ArrayStruct,
GCRT_FixedArray,
GCRT_AddStructReferencedObjects,
GCRT_AddReferencedObjects,
GCRT_AddTMapReferencedObjects,
GCRT_AddTSetReferencedObjects,
GCRT_EndOfPointer,
GCRT_EndOfStream,
};
2.2.2 FGCReferenceTokenStream
このクラスは、tokenstreamを作成し、tokenstreamからobject参照を解析するために使用されます。これは、GCのコアコンセプトと言えます。ReferenceTokenは、TArray <uint32>の形式で格納されます。この形式はなぜですか。ReferenceTokenの動作原理を分析してみましょう。FGCReferenceInfoこのクラスは、参照に必要な情報を記述し、unionンメンバー変数を持ちます。
/** Mapping to exactly one uint32 */
union
{
/** Mapping to exactly one uint32 */
struct
{
/** Return depth, e.g. 1 for last entry in an array, 2 for last entry in an array of structs of arrays, ... */
uint32 ReturnCount : 8;
/** Type of reference */
uint32 Type : 4;
/** Offset into struct/ object */
uint32 Offset : 20;
};
/** uint32 value of reference info, used for easy conversion to/ from uint32 for token array */
uint32 Value;
};
- Type:参照のタイプを表します。ここではEGCRefenceTypeOffsetです。
- Offset:クラス内のこの参照に対応する属性のアドレスオフセットを意味しています。
- ReturnCount:返されるネストの深さです。
UEは、これら3つの情報を巧みにuint32にエンコードするため、FGCReferenceTokenStreamはTArray<uint32>形式を通してtokensを保存します。
TokenStreamを処理するとき、まずreferencetokenを解析し、それからOffsetを介して属性を直接取得できます。これにより、処理が簡単になるだけでなく、キャッシュを効果的に使用して速度を上げることができます。 TokenStreamの特別な使用法もあります。これは、2つの連続するtokenを使用してポインター(64ビット)を格納することです。たとえば、実行時に、AddReferencedObjectsを実行することで参照オブジェクトを動的に追加でき、この関数のポインターはTokenStreamに保存されます。
2.2.3UClass :: AssembleReferenceTokenStream(bool bForce)メソッド
リアルタイムでトークンストリームを作成できます。一度実行するだけで、結果を保存してCLASS_TokenStreamAssembledを介してClassFlagsに反映し、計算の繰り返しを回避できます。TokenStreamが以前に作成されている場合は、古いものを置き換えます。
具体的なプロセスは次のとおりです。
(1)独自のUProperty(親クラスを除く)をトラバースし、UPropertyのEmitReferenceInfoメソッドを順番に呼び出します。これは仮想関数です。さまざまなUPropertyがそれを実装し、主にClass内の独自のメモリオフセット、ReferenceType情報をUClassに送信し、UClassはEmitObjectReferenceを通してこの参照情報をtokenにエンコードしてReferenceTokenStreamに追加します。異なるUProperty処理方法は非常に異なります。一般的なUObjectPropertyは処理が簡単ですがUArrayPropertyとUMapPropertyは、内部データ型もTokenStreamを生成する必要があるため、比較的に複雑です。structが検出されると、再帰も含まれます。
(2)このクラスに親クラスがある場合、親クラスのAssembleReferenceTokenStreamメソッドが再帰的に呼び出されて、親クラスのReferenceTokenStreamが生成され、親クラスのstreamが自分のstreamの前に追加されます。この手順は、UObjectBaseクラスまで続きます。UObjectBaseは特別な方法で処理されます。streamに追加されるのはClassPrivateとOuterPrivateのみです。
(3)自身のAddReferencedObjects()関数がUobject :: AddReferencedObjectsを指していない場合は、この関数ポインターに対応するtokenをTokenStreamに追加または更新したら、到達可能性分析を実行するときに呼び出すことができます。
(4) TokenStreamを追加したら、「EndOfStream」tokenをTokenStreamに追加し、tokens arrayにshrinkを実行し、アイドルなarray slackを削除します。これは、toneks配列の長さが次に固定される必要があるためです。 (5)ClassFlagsでCLASS_TokenStreamAssembledをtrueに設定します。
2.2.4 TFastReferenceCollector
CollectReferencesメソッドは、到達可能性分析に使用されます。シングルスレッドの場合、ProcessObjectArrayメソッドが直接呼び出され、uobjectのtoken streamをトラバースして、参照関係を見つけます。マルチスレッドの場合、uobjectリストは複数のスレッドに分割して処理され、各スレッドはProcessObjectArrayも呼び出します。
ProcessObjectArrayメソッドは、ObjectsToSerializeのUObjectをトラバースし、参照関係を見つけて、到達可能性を判断します。プロセスには、すべてがトラバースされるまで、ObjectsToSerializeは増加し続けることに注意してください。再帰的方法は内部で使用されますが、スタックを使用してシミュレートします。
(1) シングルスレッドであり、tokenstreamの自動生成がオンになっている場合、objectに対応するUClassにtokenstreamがない場合、UClassのAssembleReferneceTokenStreamsをリアルタイムで呼び出して、tokenstreamを作成します。
(2) 現在uobjectのtokenstreamを取得し、FGCReferenceInfoを解析して、参照されているUObjectを見つけます。
tokenのReferenceInfoはさまざまなタイプがあり、状況に応じて処理する必要があります。GCRT_ObjectやGCRT_ArrayObjectのように扱いやすくて、uobjectオブジェクトをObjectsToSerializeに追加するだけで済みます。 GCRT_ArrayStructはより厄介で、再帰的な処理が必要です。ここでの「struct」は、C ++のstruct構造体だけでなく、UEdGraphPinなどのUObjectシステムに属していない一部のclassも指します。GCRT_ArrayStructを処理するときは、最初に再帰スタックをインクリメントしてから、Array内の「Struct」を1つずつ処理する必要があります。 GCRT_AddStructReferencedObjectsは、structやFGCObjectから継承しないclassがUOBjectへの参照も追加できることを示します。UStructProperty:: EmitReferenceInfoのコードは、structpropertyが参照を追加できることも示しています。しかし、コードとコメントを見ると、UE4は将来これらの特別なstructとclassをFGCObjectから継承させ、AddReferencedObjects関数を使用して参照を追加するかもしれないと思います。 GCRT_AddReferencedObjectsは、参照を追加するためにこのオブジェクトのAddReferencedObjects関数を呼び出す必要があることを意味します。 FGCObjectを回想すると、このクラスはUObjectを継承しませんが、AddReferencedObjects関数を介してUObjectに参照を追加することもできます。同時に、この関数はUClassによってのみTokenStreamに追加できます。FGCObjectはどのように機能しますか?実際、UEにはUGCObjectを管理するための特別なUObjectインスタンスがあります。これはUGCObjectReferencerです。このクラスのAddReferencedObjects関数を見てください。
void UGCObjectReferencer::AddReferencedObjects(UObject* InThis, FReferenceCollector& Collector)
{
UGCObjectReferencer* This = CastChecked<UGCObjectReferencer>(InThis);
// Note we're not locking ReferencedObjectsCritical here because we guard
// against adding new references during GC in AddObject and RemoveObject.
// Let each registered object handle its AddReferencedObjects call
for (FGCObject* Object : This->ReferencedObjects)
{
check(Object);
Object->AddReferencedObjects(Collector);
}
Super::AddReferencedObjects( This, Collector );
}
このクラスのインスタンスが参照・マークの段階で収集されると、FGCObjectのAddReferencedObjectsメソッドを1つずつ呼び出してUObject参照を収集し、それによってFGCObjectをGCシステムに組み込みます。
(3) 参照されたUObjectを取得した後、通常はUObjectへの参照を追加し、ObjectsToSerialize配列に追加します。
UObjectがすでにisPendingKillとマークされている場合、それが参照されていても無視されます。
マーキングは複数のスレッドで実行できるため、2つのスレッドが同時にオブジェクトを到達可能としてマークし、ObjectsToSerialize配列に追加して参照チェックを続行することができますが、これは明らかな無駄です。したがって、オブジェクトをマークするとき、オブジェクトが現在Unreachableであるかどうかをチェックするだけでなく、Unreachableフラグをパージするには、2つのスレッドが誤って同時に設定するのを防ぐために、アトミックな「比較と置換」操作も必要です。
UObjectがCluster内にある場合は、それをReachableInClusterとしてマークし、必要に応じてそのClusterOwnerを到達可能としてマークし、後続の処理のためにObjectsToSerializeに追加します。
(4) ObjectsToSerialize配列のスキャンはラウンドごとに実行され、1ラウンドのスキャン中にスキャンされた新しいUObjectは一時的にNewObjectsToSerialize配列に格納されます。このラウンドのスキャンが完成すると、NewObjectsToSerializeにある要素の数がMinDesiredObjectsPerSubTaskというしきい値に達すると、マルチスレッドが始まります。しきい値に達していない場合、現在のスレッドで新しいラウンドの処理が続行します。
2.2.5パージするUobjectのリストを取得する
まずClusterの概念を紹介します。
複合論理オブジェクトに対し、内部Objectは親オブジェクトの状態で管理されるので、ClusterでGC管理をすることができます。Clusterは、GCプロセスで単一のユニットとして扱われ、GCを高速化できるUObjectのグループです。パーティクルシステムでは、1組のパーティクルオブジェクトはClusterとしてマークされます。Actorは、「can be in Cluster」を設定することで、自分自身をあるClusterに追加することができます。
プログラムで、Cluster はFUObjectClusterというクラスとして表示されます。 ObjectsのプロパティはCluster内のすべてのObjectsで、ReferencedClustersはこのObjectsによって参照される他のObjectsです。
Clusterによって参照される他のClusterルートオブジェクトがPendingKillとしてマークされている場合、他のpendingkill参照を処理するには、このCluster内のすべてのオブジェクトをObjectsToSerialize配列に追加する必要があります。同時に、Cluster間の参照関係が保証されなくなったため、Clusterは分解が必要であるとマークされます。
到達可能なオブジェクトを分析した後、分解のマークが付けられたClusterを分解する必要があります。
その後、すべてのUObjectを再度スキャンして、到達不能なすべてのオブジェクトを収集する必要があります。この操作は、複数のスレッドで並行して処理することもできます。到達不能オブジェクトの場合、それらがCluster内にない場合、それらはGUnreachableObjects配列に直接追加されます。 ClusterRootの場合は、含まれているすべてのObjectsを分析する必要があります。ObjectにClusterの外部からの参照がない場合も、到達不能であるため、GUnreachableObjects配列に追加する必要があります。これまでのところ、GUnreachableObjects配列のUObjectは、このスキャンから得たパージする必要があるオブジェクトです。
2.3 GCロックを解放する
GCロックを削除し、他のスレッドがUObjectを使用できるようにします。解放プロセスは取得プロセスよりもはるかに簡単です。
(1)GCUnlockedEvent-> Trigger()を呼び出して、このeventを待機しているスレッドをウェイクアップします。
(2)GCCounterがデクリメントされます GC状態を終了します。
UWA公式サイト:https://jp.uwa4d.com
UWA公式ブログ:https://blog.jp.uwa4d.com
UWA公式Q&Aコミュニティ(中国語注意):https://answer.uwa4d.com
これも興味あるかも
-
原理から応用まで ゲームでの動的解像度
January 4, 2023 -
Unityゲームの使用メモリを最適化しよう
December 21, 2022 -
ASTC テクスチャ圧縮形式の紹介
December 14, 2022