UE 补完计划(二) GC-2
前言
上节说到, MarkObjectsAsUnreachable() 与 PerformReachabilityAnalysisOnObjectsInternal() 是执行标记阶段两个步骤分别对应的函数,本节我们就来分析它们。
MarkObjectsAsUnreachable
由于这个函数比较长(200行左右),这里就不放出完整的函数内容而使用分段分析的方法,感兴趣的读者可以自行在 UObject/GarbageCollection.cpp 中查看该函数。
首先查看函数注释与签名,可以看出,该函数的作用为:将将已有的对象列表中的对象,标记为可达/不可达。并且根据 <bool bParallel, bool bWithClusters> 两个模板参数可以实例化出不同的函数以适用于各种情况。
1 | /** |
首先,该函数会计算出当前 GUObjectArray 中需要被 GC 的对象的数量,并根据线程数将这些对象平均分配给每个线程来处理。GUObjectArray 是一个定义在 UObjectHash.cpp 中的数组,用于存放全局的对象,GUObjectArray 的前部存储了一些不纳入GC的 object,因此扫描的object列表中会去掉前面这些object,只考虑后面的,得到MaxNumberOfObjects。
随后定义了两个 FIFO 的数据结构(类似于队列),分别是 ClustersToDissolveList 与 KeepClusterRefsList,这两个数组具体的作用会在之后进行分析。
此外,函数为每个线程都申请了一个 FGCArrayStruct 结构体,因此 ObjectsToSerializeArrays 是一个二维数组,想必这些结构体就是用来装每个线程要处理的对象的。
1 | const EInternalObjectFlags FastKeepFlags = EInternalObjectFlags::GarbageCollectionKeepFlags; |
这里先说结论:
ObjectsToSerializeArrays数组用于存放所有可达的对象KeepClusterRefsList数组用于存放簇中可达的对象ClustersToDissolveList数组用于存放簇中不可达的对象
其中,KeepClusterRefsList 与 ClustersToDissolveList 只会在当前 GC 启用了簇之后才会进行统计。结合后续的处理过程,可以更好地理解它们的作用。
随后是一个并行的 for 循环,在多线程条件下遍历GUobjectArray中的对象。从注释中可以看到,似乎标记阶段检查的 flag 就是 UObjectArray 结构体的一部分,因此可以降低 cache misses,缓存和缓存命中准备开个新坑再细说(标记一下以防忘掉)。
1 | // Iterate over all objects. Note that we iterate over the UObjectArray and usually check only internal flags which |
ParallelFor() 的第二个参数是一个非常巨大的 lambda 函数,由对应的函数签名可以看出,这个匿名函数 PerParticleFunction 就是每个线程需要执行的函数了。
1 | static void ParallelFor(const int32 Size, std::function<void(int32)> PerParticleFunction) |
从捕获参数列表中我们可以看到,这个匿名函数捕获了上述定义的三个数组。其中,ClustersToDissolveList 与 KeepClusterRefsList 是引用捕获,ObjectsToSerializeArrays 是值捕获。
1 | [ObjectsToSerializeArrays, &ClustersToDissolveList, &KeepClusterRefsList, ...] |
GetOwnerIndex()
在分析这个函数之前,需要了解一些额外的知识。
在 GCObjectInfo.h 中,有一个名为 FGCObjectInfo 的类,该类的作用是保存 UObject 的一些信息以用于 GC,这样可以避免对于 UObject 的直接访问。
1 | /** |
其中有一个成员变量名为 ClusterRootIndex,并且默认值被赋予了 -1。而 GetOwnerIndex() 则会返回 ClusterRootIndex。我们可以做出如下的猜测:
在 GC 的设置中,如果启用了 Cluster,那么会对当前的对象进行划分,将它们整合到不同的簇中,而簇的标识就是 ClusterRootIndex,这个值表示了当前对象所在簇的根节点下标。如果该值为 -1,则表明当前的对象为簇的根节点;相反地,如果该值不为 -1(判断条件可以是 >= 0,因为 ClusterRootIndex 是一个下标,如果不是初始值的话必然 >= 0),则说明当前对象处于一个以其他对象为根节点的簇中。
如此说来,这莫非是一个类似于并查集的数据结构?
在 PerParticleFunction 这个匿名函数中,会用到很多次对于 GetOwnerIndex() 的条件判断。
PerParticleFunction
回到正题,在这个函数中,会针对对象进行不同情况的不同处理:
-
对象属于根集
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15// Special case handling for objects that are part of the root set.
if (ObjectItem->IsRootSet())
{
// ...
if (bWithClusters)
{
if (ObjectItem->HasAnyFlags(EInternalObjectFlags::ClusterRoot) || ObjectItem->GetOwnerIndex() > 0)
{
KeepClusterRefsList.Push(ObjectItem);
}
}
LocalObjectsToSerialize.Add(Object);
}如果对象属于根集,那么一定是可达的,因此无论如何都会被添加进
LocalObjectsToSerialize,我们可以在 for 循环的外部看到这个数组的定义,就是ObjectsToSerializeArrays对应当前线程的可达对象数组。1
TArray<UObject*>& LocalObjectsToSerialize = ObjectsToSerializeArrays[ThreadIndex]->ObjectsToSerialize;
此外,如果启用了簇,即
bWithClusters == true,在当前的对象为一个簇的根(ClusterRoot)或GetOwnerIndex() > 0时,将对象添加进KeepClusterRefsList。从上文的分析中我们可以得知,GetOwnerIndex() > 0意味着当前对象处于以其他对象为根的簇中。结合这两个条件可以知道,当前对象一定是可达的,因此添加进KeepClusterRefsList数组。 -
常规对象
第二个判断条件是:没有启用簇或者当前对象为簇的根节点。由于第一个判断处理了所有根对象的情况,因此这里处理的都是普通对象。
首先,会假设当前对象不可达,如果当前对象带有一些指定的 flag 或者本身不是
PendingKill等情况就会标记为可达。如果当前对象被标记为PendingKill或Garbage且当前对象为ClusterRoot,则会将该对象添加进ClustersToDissolveList。最后会对
bMarkAsUnreachable做检查,如果对象可达,则会被添加进LocalObjectsToSerialize,如果启用了簇且当前对象为某个簇的根则对象会被添加进KeepClusterRefsList。如果对象不可达则会设置对象的Unreachable标志位。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41// Regular objects or cluster root objects
else if (!bWithClusters || ObjectItem->GetOwnerIndex() <= 0)
{
bool bMarkAsUnreachable = true;
// Internal flags are super fast to check and is used by async loading and must have higher precedence than PendingKill
if (ObjectItem->HasAnyFlags(FastKeepFlags))
{
bMarkAsUnreachable = false;
}
// If KeepFlags is non zero this is going to be very slow due to cache misses
else if (!ObjectItem->IsPendingKill() && KeepFlags != RF_NoFlags && Object->HasAnyFlags(KeepFlags))
{
bMarkAsUnreachable = false;
}
PRAGMA_DISABLE_DEPRECATION_WARNINGS
else if (ObjectItem->HasAnyFlags(EInternalObjectFlags::PendingKill | EInternalObjectFlags::Garbage) && bWithClusters && ObjectItem->HasAnyFlags(EInternalObjectFlags::ClusterRoot))
PRAGMA_ENABLE_DEPRECATION_WARNINGS
{
ClustersToDissolveList.Push(ObjectItem);
}
// Mark objects as unreachable unless they have any of the passed in KeepFlags set and it's not marked for elimination..
if (!bMarkAsUnreachable)
{
// IsValidLowLevel is extremely slow in this loop so only do it in debug
checkSlow(Object->IsValidLowLevel());
LocalObjectsToSerialize.Add(Object);
if (bWithClusters)
{
if (ObjectItem->HasAnyFlags(EInternalObjectFlags::ClusterRoot))
{
KeepClusterRefsList.Push(ObjectItem);
}
}
}
else
{
ObjectItem->SetFlags(EInternalObjectFlags::Unreachable);
}
} -
簇中的常规对象
最后,针对簇中的常规对象,如果有特定的 flag,则会被添加进两个数组中。
1
2
3
4
5
6
7
8
9
10// Cluster objects
else if (bWithClusters && ObjectItem->GetOwnerIndex() > 0)
{
// treat cluster objects with FastKeepFlags the same way as if they are in the root set
if (ObjectItem->HasAnyFlags(FastKeepFlags))
{
KeepClusterRefsList.Push(ObjectItem);
LocalObjectsToSerialize.Add(Object);
}
}
到此为止,多线程的 for 循环基本就结束了,也完成了对于 GUObjectArray 中所有参与 GC 的对象 可达/不可达 的标记。接下来就是对于三个列表的不同处理。
首先是 ObjectsToSerializeArrays,该二维数组存放的是每个线程收集到的可达的对象。这里会统计全部可达对象的数量,并且将该数组内的对象移交给 ObjectsToSerialize 数组,最后再向 FGCArrayPool 归还列表空间。
1 | // Collect all objects to serialize from all threads and put them into a single array |
ObjectsToSerialize 不知道大家还有没有印象,这就是 MarkObjectsAsUnreachable() 的参数之一,可以参考上一节的内容:UE 补完计划(二) GC-1,我们在 PerformReachabilityAnalysis() 中申请了一个名为 ObjectsToSerialize 的数组并且将它传递给了 MarkObjectsAsUnreachable 函数。
1 | template <bool bParallel, bool bWithClusters> |
由于 ObjectsToSerialize 是引用传递,因此可以将所有可达对象传递给上层的函数,即 PerformReachabilityAnalysis()。
然后是对 ClustersToDissolveList 的处理,DissolveClusterAndMarkObjectsAsUnreachable 函数可以解散当前对象所属的集群(簇,也就是cluster),并且执行真正的“标记对象为不可达”。准确的说是调用SetFlags(EInternalObjectFlags::Unreachable);来标记对象。
1 | if (bWithClusters) |
最后是对 KeepClusterRefsList 的处理。前面说过,这个列表存放的是可达的簇中的对象,因此这里也会遍历数组并且将相关的对象全都标记为可达。
1 | if (bWithClusters) |
有一点需要注意的是,MarkObjectsAsUnreachable() 向上级函数,也就是 PerformReachabilityAnalysis() 传递信息的方式只有 ObjectsToSerialize 数组,而后续对于 ClustersToDissolveList 与 KeepClusterRefsList 数组中对象的标记也会反映在 ObjectsToSerialize 数组中。
到这里为止,MarkObjectsAsUnreachable() 函数的内容也就基本分析完毕了。
小结
本节主要分析了标记的第一阶段 MarkObjectsAsUnreachable() 的内容,这个函数的主要作用就是遍历 GUObjectArray 中的所有参与 GC 的对象,并将收集到的所有可达对象传递给 ObjectsToSerialize 数组,该数组也是一个引用传递的函数参数,因此可以将信息传递到更上层的 PerformReachabilityAnalysis() 函数中。
此外,该函数还会对簇中的对象做进一步的处理与标记,但最终的标记结果都会体现在 ObjectsToSerialize 中。



