UE4GC中一种标记流程使用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列表。
第一步,我们可以向ObjectsToSerialize添加FGCObject::GGCObjectReferencer
后者是一个静态的UGCObjectReferencer,添加后可用于在非UObject对象上调用AddReferencedObjects方法。
第二步,调用MarkObjectsAsUnreachable方法,把所有不带KeepFlags和EInternalObjectFlags::GarbageCollectionKeepFlags标记的对象标记为不可达
首先,这里涉及到GUObjectArray这个变量,这是一个全局的Uobject allocator,其中的ObjObjects数组保存了所有的UObject(通过FUObjectItem进行封装),UObjectBase::InternalIndex属性就是对象对应的FUObjectItem在数组中的下标,因此可以方便的根据下标找到UObject或者通过UObject找到对应下标。
GUObjectArray中前部存储了一些不纳入GC的object,因此scan的object列表中会去掉前面这些object,只考虑后面的,得到MaxNumberOfObjects。具体哪些对象不被GC考虑,可以查看FUObjectArray的实现。
接下来就需要对这些uobject进行不可达标记,这里使用了多线程版本的For循环。多线程执行的原理并不复杂,首先可以获取当前可用的工作线程,然后把待标记的object平均分配给这些线程进行遍历,多线程底层使用了UE的GraphTask框架。在对一个uobject进行标记时,正常情况下都读对应的FUObjectItem中属性,特殊情况才读uobject,因为FUObjectItem是一个结构体,而且在GUObjectArray中紧密排列,所以在顺序遍历下是缓存友好的。
值得一提的是,UE使用了簇(Cluster)来提高效率,具体如何提高会在下面介绍。如果一个object属于RootSet,则直接加入到ObjectsToSerializeList中,如果是ClusterRoot或在Cluster中,也加入到KeepClusterRefsList列表中。如果object的ClusterRootIndex<=0(不在cluster中或者为ClusterRoot),则先根据是否有KeepFlags,判断是否要标记为不可达,如果不要标记,则把object加到ObjectsToSerializeList中,且如果为ClusterRoot就加入到KeepClusterRefsList中,如果要标记,则加入到ClustersToDissolveList中,且对ObjectItem设置Unreachable标记。会对Cluster做一些额外的处理,细节可看代码。
第三步,调用PerformReachabilityAnalysisOnObjects来判断uobject可达性
这里会用到FGCReferenceProcessor,TFastReferenceCollector,FGCCollector这几个类,都同时支持单线程和多线程。
先介绍一下ReferenceToken概念
在UObject体系中,每个类有一个UClass实例用于描述该类的反射信息,使用UProperty可描述每个类的成员变量,但在GC中如果直接遍历UProperty来Scan
对象引用关系,效率会比较低(因为存在许多非Object引用型Property),所以UE创建了ReferenceToken,它是一组toke流,描述类中对象的引用情况。下Scan中列举了引用的类型:
/**
* 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,
};
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:引用的类型,就是EGCRefenceType
Offset:这个引用对应的属性在类中的Address偏移
ReturnCount:返回的嵌套深度
UE巧妙的把这3个信息编码成了一个uint32,因此FGCReferenceTokenStream可以通过TArray<uint32>形式存储tokens。
当我们处理TokenStream时,可以先从中解析出一个个referencetoken,然后通过Offset直接获取属性,不仅处理起来更简单,更能有效利用缓存,加快速度。
TokenStream还有一种特殊的用法,就是用两个连续的token来存储一个指针(64位),比如运行时可以通过执行AddReferencedObjects来动态添加引用的对象,而这个函数的指针就储存在TokenStream中。
UClass::AssembleReferenceTokenStream(bool bForce)方法
可以实时创建tokenstream,只需执行一次,就能把结果保存下来,并在ClassFlags中通过CLASS_TokenStreamAssembled进行体现,防止重复计算。如果之前已经创建过TokenStream,就替换调旧的。
具体流程为:
-
遍历自身的UProperty(不包括父类的),依次调用UProperty的EmitReferenceInfo方法。这是一个虚函数,不同的UProperty会实现它,主要会把自己在Class中的内存偏移,ReferenceType信息发送给UClass,UClass再通过EmitObjectReference把这个引用信息编码成token,加入到ReferenceTokenStream中。不同的UProperty处理方式有很大区别,普通的UObjectProperty比较好处理,UArrayProperty和UMapProperty就比较复杂,因为它们内部的数据类型也需要生成TokenStream,如果碰到struct,还会涉及到递归。
-
如果这个类有父类,则递归调用父类的AssembleReferenceTokenStream方法,生成父类的ReferenceTokenStream,并把父类的stream添加到自己的stream之前。这个步骤会一直到UObjectBase这个类为止,UObjectBase的处理方式比较特殊,只会把ClassPrivate和OuterPrivate添加到stream中。
-
如果自身的AddReferencedObjects()函数不是指向Uobject::AddReferencedObjects,则向TokenStream中加入或更新这个函数指针对应的token,在执行可达性分析时即可调用到这个函数了。
-
TokenStream添加完毕,把"EndOfStream"token添加到TokenStream,并对tokens array进行shrink,去掉空闲的array slack,因为toneks数组长度在接下来应该是固定的。
-
ClassFlags中把CLASS_TokenStreamAssembled设为true。
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结构体,一些不属于UObject体系的class也算,比如UEdGraphPin。处理GCRT_ArrayStruct时,需要先把递归的栈递增,然后逐个处理Array中的"Struct"。
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中有一个专门的UObject实例来管理FGCObject,就是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方法,搜集UObject引用,从而把FGCObject纳入到GC体系中。
3. 得到被引用的UObject后,一般会对其添加引用,并加入到ObjectsToSerialize数组中。
如果一个UObject已经被标记为isPendingKill了,那么即使它被引用到,也会忽略。
由于标记可以多线程进行,因此有可能两个线程同时对一个对象标记为可达,并加入到ObjectsToSerialize数组,继续进行引用检查,这显然是浪费的。因此对一个对象进行标记时,不仅要检查这个对象当前是否为Unreachable,清理它的Unreachable标记也要有一个原子的“比较再替换”操作,防止两个线程碰巧同时设置。
如果这个UObject在Cluster中,则把它标记为ReachableInCluster,同时如果需要也把它的ClusterOwner标记为可达,并加入到ObjectsToSerialize中做后续处理。
4. 对ObjectsToSerialize数组的Scan会一轮一轮进行,一轮Scan过程中Scan到的新的UObject会暂时存放在NewObjectsToSerialize数组中,当对ObjectsToSerialize一轮Scan完时,如果NewObjectsToSerialize中元素数量超过MinDesiredObjectsPerSubTask这一阈值,则新开多个线程处理,如果不到阈值,则在当前线程中继续新的一轮处理。