远远围墙,隐隐茅堂。
飏青旗、流水桥旁。
Cluster中文叫“簇”,统一管理UE中将多个生命周期相同的UObject,以优化GC性能。
Cluster的作用和原理
区别于其他编程语言中普适性的GC,UE作为游戏引擎,其GC机制也与游戏息息相关。比如UE结合游戏分帧执行的特性,GC机制使用了分帧清理。同时游戏中也存在一些生死相关的物体,在GC标记时可以一起被标记为可达或不可达。
在游戏开发中,有一些生命周期相同的物体,如果按照常规的可达性分析逻辑进行遍历的话,同生共死的父物体和子物体会一起被遍历。
基本的结构如下图:
显然,这些生命周期一致的物体可以一同被标记为可达/不可达,而不是继续深搜/广搜标记,这样可以有效减少GC标记阶段的耗时。如下图中深色的对象(O2-O8),其生命周期一致,可一同被标记为可达。
在 UE 中就使用了一种结构对生命周期一致的物体进行组织,这种结构叫做Cluster。当然,Cluster并不能非常广泛的用在所有的 UObject 上,适用 Cluster 的有粒子系统,蓝图节点等。
Cluster的结构和存储
Cluster相关的设置在Project Settings中。
结构
Cluster在UE中使用类 FUObjectCluster
来表示,管理和存储则使用 FUObjectClusterContainer
类。
/** UObject cluster. Groups UObjects into a single unit for GC. */
struct FUObjectCluster
{
FUObjectCluster()
: RootIndex(INDEX_NONE)
, bNeedsDissolving(false)
{}
/** Root object index */
int32 RootIndex;
/** Objects that belong to this cluster */
TArray<int32> Objects;
/** Other clusters referenced by this cluster */
TArray<int32> ReferencedClusters;
/** Objects that could not be added to the cluster but still need to be referenced by it */
TArray<int32> MutableObjects;
/** List of clusters that direcly reference this cluster. Used when dissolving a cluster. */
TArray<int32> ReferencedByClusters;
/** Cluster needs dissolving, probably due to PendingKill reference */
bool bNeedsDissolving;
};
class COREUOBJECT_API FUObjectClusterContainer
{
/** List of all clusters */
TArray<FUObjectCluster> Clusters;
/** List of available cluster indices */
TArray<int32> FreeClusterIndices;
/** Number of allocated clusters */
int32 NumAllocatedClusters;
/** Clusters need dissolving, probably due to PendingKill reference */
bool bClustersNeedDissolving;
/** Dissolves a cluster */
void DissolveCluster(FUObjectCluster& Cluster);
public:
...
}
关注的信息:
FUObjectCluster::RootIndex
表示当前 Cluster 在FUObjectClusterContainer::Clusters
中的下标FUObjectCluster::Objects
记录哪些 UObject 属于当前 ClusterFUObjectCluster::ReferencedClusters
记录当前 Cluster 引用的其他 Cluster 的下标FUObjectCluster::ReferencedByClusters
记录当前 Cluster 被哪些 Cluster 引用,应该是用于实现向上递归FUObjectCluster::MutableObjects
记录被当前 Cluster 引用但不属于当前 Cluster 的 UObjectFUObjectCluster::bNeedsDissolving
则表示该 Cluster 是否应该被解组,在GC过程中被读写
在 UObjectArray.h
中会声明一个 FUObjectClusterContainer
类的全局变量 GUObjectClusters
,用于管理所有的Cluster,貌似就那一个对象,为什么不用单例呢?
同时在 FUObjectItem
中有一个 ClusterRootIndex
整数成员记录自己属于哪个Cluster。
- 如果该值为负数,则通过 ` -ClusterRootIndex - 1` 计算所属 Cluster 的下标
- 如果该值为正数( > 1),则表示当前 UObject 为 Cluster Root,同时
ClusterRootIndex
表示对应 Cluster 的下标 - 该值为0,该 UObject 不属于任何 Cluster
Cluster与UObject
这里有几个问题,什么样的 UObject 能被加入到 Cluster 中,又是什么样的 UObject 能作为 Cluster Root。
这个问题在类 UObjectBaseUtility
中可以得到部分答案。
从上图中可以看出,默认情况下,UObject 不能作为 Cluster Root,需要的类需要自行实现 CanBeClusterRoot()
虚函数。
同时 UObject 根据 Outer 来进行判断是否可以加入到 Cluster 中,否则默认可以。
有区别的是,AActor 以及子类则默认不加入到 Cluster 中,可以在编辑器中进行勾选。
部分其他子类重写 CanBeInCluster()
成员函数来修改是否能加入到 Cluster 中。
默认返回false的有:UMaterialParameterCollection
、UMediaPlayer
、UMediaPlaylist
、USoundBase
、USoundCue
、USoundNode
,也就是说这些类及子类不能直接加入到 Cluster中。
默认情况下,UObject不可以作为Cluster Root,子类可以重写 CanBeClusterRoot()
让自己可以作为 Cluster Root。
引擎中默认情况下可以作为 Cluster Root的类有:UMaterial
,UmiMaterial
,UParticleSystem
。
类 UBlueprintGeneratedClass
需要在项目设置中勾选 Blueprint Clustering Enabled
。
如果在项目设置中勾选了 Actor Clustering Enabled
,则会通过 ULevelActorContainer
类创建当前关卡的 Cluster,尽管 ULevel::CanBeClusterRoot()
永远返回 false。
Cluster的创建与解组
创建
Cluster 创建有个默认的方法是 UObjectBaseUtility::CreateCluster()
,其他子类会对该函数进行重写。
void UObjectBaseUtility::CreateCluster()
{
FUObjectItem* RootItem = GUObjectArray.IndexToObject(InternalIndex);
if (RootItem->GetOwnerIndex() != 0 || RootItem->HasAnyFlags(EInternalObjectFlags::ClusterRoot))
{
return;
}
const int32 ClusterIndex = GUObjectClusters.AllocateCluster(InternalIndex);
FUObjectCluster& Cluster = GUObjectClusters[ClusterIndex];
Cluster.Objects.Reserve(64);
// Collect all objects referenced by cluster root and by all objects it's referencing
FClusterReferenceProcessor Processor(InternalIndex, Cluster);
TFastReferenceCollector<
FClusterReferenceProcessor,
TDefaultReferenceCollector<FClusterReferenceProcessor>,
FGCArrayPool,
EFastReferenceCollectorOptions::AutogenerateTokenStream | EFastReferenceCollectorOptions::ProcessNoOpTokens
> ReferenceCollector(Processor, FGCArrayPool::Get());
FGCArrayStruct ArrayStruct;
TArray<UObject*>& ObjectsToProcess = ArrayStruct.ObjectsToSerialize;
ObjectsToProcess.Add(static_cast<UObject*>(this));
ReferenceCollector.CollectReferences(ArrayStruct);
check(RootItem->GetOwnerIndex() == 0);
RootItem->SetClusterIndex(ClusterIndex);
RootItem->SetFlags(EInternalObjectFlags::ClusterRoot);
if (Cluster.Objects.Num() >= GUObjectClusters.GetMinClusterSize())
{
// Add new cluster to the global cluster map.
Cluster.Objects.Sort();
Cluster.ReferencedClusters.Sort();
Cluster.MutableObjects.Sort();
}
else
{
for (int32 ClusterObjectIndex : Cluster.Objects)
{
FUObjectItem* ClusterObjectItem = GUObjectArray.IndexToObjectUnsafeForGC(ClusterObjectIndex);
ClusterObjectItem->SetOwnerIndex(0);
}
GUObjectClusters.FreeCluster(ClusterIndex);
check(RootItem->GetOwnerIndex() == 0);
check(!RootItem->HasAnyFlags(EInternalObjectFlags::ClusterRoot));
}
}
函数执行如下过程:
- 创建空的 Cluster,并将当前的 UObject 作为 Cluster Root
- 创建引用收集的对象
ReferenceCollector
,并将当前 UObject 存入到ObjectsToSerialize
中,调用TFastReferenceCollector::CollectReference()
完成引用收集,此时所有被当前 UObject 引用到的 UObject 对象都会被收集到对应的Cluster中,具体可以查阅HandleTokenStreamObjectReference()
函数,其实也就是可达性分析的过程。 - 如果新 Cluster 中成员的数量大于等于用户设定的值,则将新 Cluster 进行排序,然后加入到 Cluster 集合中
- 如果新 Cluster 中成员的数量大于用户设定的值,则解组新 Cluster
不同类或者不同的 Cluster Root 执行不同的创建 Cluster 逻辑,以下介绍ULevel::CreateCluster()
- > ULevelActorContainer::CreateCluster()
。
void ULevel::CreateCluster()
{
if (FPlatformProperties::RequiresCookedData() && GCreateGCClusters && GActorClusteringEnabled && !bActorClusterCreated)
{
TArray<AActor*> ClusterActors;
for (int32 ActorIndex = Actors.Num() - 1; ActorIndex >= 0; --ActorIndex)
{
AActor* Actor = Actors[ActorIndex];
if (Actor && Actor->CanBeInCluster())
{
ClusterActors.Add(Actor);
}
else
{
ActorsForGC.Add(Actor);
}
}
if (ClusterActors.Num())
{
ActorCluster = NewObject<ULevelActorContainer>(this, TEXT("ActorCluster"), RF_Transient);
ActorCluster->Actors = MoveTemp(ClusterActors);
ActorCluster->CreateCluster();
}
bActorClusterCreated = true;
}
}
将 Level 中的 Actor 按照是否能加入到 Cluster 中,分别添加到 ActorsForGC
和 ClusterActors
中,然后调用 ULevelActorContainer::CreateCluster()
真正创建 Cluster。
ULevelActorContainer::CreateCluster()
的实现与 UObjectBaseUtility::CreateCluster()
没有太大差别,不再赘述。
所以 Level 中 Actor 的GC其实需要根据开发者/用户配置来决定是否通过 Cluster 执行。类 ULevelActorContainer
就辅助 ULevel
对加入到 Cluster 中的UObject进行GC,其他的 Actor 仍然参与到普通的GC流程中。
加入Cluster
主要函数是 UObjectBaseUtility::AddToCluster()
,也就是由 UObject 自身调用,同时参数决定是否要加入到 MutableObjects
中。
也就是分成两种情况:
- 不加入到
MutableObjects
中,这里会通过TFastReferenceCollector::CollectReference()
执行引用收集,将所有被当前 UObject 引用到的所有 UObject 都加入到 Cluster 中,这个过程和上述 Cluster 创建的过程非常相似,不再赘述。 - 加入到
MutableObjects
中,这里不是简单的将 UObject 对应在GUObjectArray
中的 Index 插入到MutableObjects
中就完事,同时也要保持相对有序,也就是说MutableObjects
必须是严格递增的序列,且没有重复的 Index,应该是要利用单调栈的性质来做点事情。。。
解组
解组的实现在 FUObjectClusterContainer::FreeCluster()
和 FUObjectClusterContainer::DissolveCluster
中。
调用顺序是 FUObjectClusterContainer::DissolveCluster()
-> FUObjectClusterContainer::FreeCluster()
。
void FUObjectClusterContainer::DissolveCluster(FUObjectCluster& Cluster)
{
FUObjectItem* RootObjectItem = GUObjectArray.IndexToObjectUnsafeForGC(Cluster.RootIndex);
// Unreachable or not, we won't need this array later
TArray<int32> ReferencedByClusters = MoveTemp(Cluster.ReferencedByClusters);
// Unreachable clusters will be removed by GC during BeginDestroy phase (unhashing)
if (!RootObjectItem->IsUnreachable())
{
#if UE_GCCLUSTER_VERBOSE_LOGGING
UObject* ClusterRootObject = static_cast<UObject*>(RootObjectItem->Object);
UE_LOG(LogObj, Log, TEXT("Dissolving cluster (%d) %s"), RootObjectItem->GetClusterIndex(), *ClusterRootObject->GetFullName());
#endif // UE_GCCLUSTER_VERBOSE_LOGGING
const int32 OldClusterIndex = RootObjectItem->GetClusterIndex();
for (int32 ClusterObjectIndex : Cluster.Objects)
{
FUObjectItem* ClusterObjectItem = GUObjectArray.IndexToObjectUnsafeForGC(ClusterObjectIndex);
ClusterObjectItem->SetOwnerIndex(0);
}
FreeCluster(OldClusterIndex);
}
// Recursively dissolve all clusters this cluster is directly referenced by
for (int32 ReferencedByClusterRootIndex : ReferencedByClusters)
{
FUObjectItem* ReferencedByClusterRootObjectItem = GUObjectArray.IndexToObjectUnsafeForGC(ReferencedByClusterRootIndex);
if (ReferencedByClusterRootObjectItem->GetOwnerIndex())
{
DissolveCluster(Clusters[ReferencedByClusterRootObjectItem->GetClusterIndex()]);
}
}
}
很明显,DissolveCluster()
是一个递归函数,首先会判断当前 Cluster 的 Cluster Root 是否可达,如果不可达,则遍历 Cluster 中所有 UObject,设置对应的FUObjectItem::ClusterRootIndex
为 0,表示该 UObject 不属于任何一个 Cluster,处理完 Cluster 中所有 UObject 后,调用 FreeCluster()
继续收拾对应的 Cluster。后面则遍历每个引用当前 Cluster(子) 的 Cluster(父),然后递归调用。所以这实际上是一个向上递归的解组过程。
void FUObjectClusterContainer::FreeCluster(int32 InClusterIndex)
{
FUObjectCluster& Cluster = Clusters[InClusterIndex];
FUObjectItem* RootItem = GUObjectArray.IndexToObject(Cluster.RootIndex);
RootItem->SetOwnerIndex(0);
RootItem->ClearFlags(EInternalObjectFlags::ClusterRoot);
for (int32 ReferencedClusterRootIndex : Cluster.ReferencedClusters)
{
if (ReferencedClusterRootIndex >= 0)
{
FUObjectItem* ReferencedClusterRootItem = GUObjectArray.IndexToObjectUnsafeForGC(ReferencedClusterRootIndex);
if (ReferencedClusterRootItem->GetOwnerIndex() < 0)
{
FUObjectCluster& ReferencedCluster = Clusters[ReferencedClusterRootItem->GetClusterIndex()];
ReferencedCluster.ReferencedByClusters.Remove(Cluster.RootIndex);
}
}
}
Cluster.RootIndex = INDEX_NONE;
Cluster.Objects.Reset();
Cluster.MutableObjects.Reset();
Cluster.ReferencedClusters.Reset();
Cluster.ReferencedByClusters.Reset();
Cluster.bNeedsDissolving = false;
FreeClusterIndices.Add(InClusterIndex);
NumAllocatedClusters--;
}
FUObjectClusterContainer::FreeCluster()
的实现相当于将对应的 Cluster 重置,同时会遍历当前 Cluster 所引用的其他 Cluster,在其他 Cluster 中接触对当前 Cluster 的引用。
解组会发生在GC阶段。
GC中的Cluster
Cluster 本就是服务于GC,以实现GC Mark阶段的加速,所以大部分对Cluster的操作都在GC中进行。
之前在介绍UE GC的Mark和Sweep时,就在源码中看到了对Cluster的遍历和解组,但是没有展开说,这里继续解析。
回到 CollectGarbageInternal()
函数,其中第一处出现 Cluster 的地方是用户在命令行中手动关闭 Cluster,这里会直接触发 Cluster 解组。
在可达性分析阶段,进行引用收集之前,会先将 UObject 标记为不可达,这个过程中同时也进行了 Cluster 的更新,更新包括两方面。
- Cluster 中有标记为 PendingKill 的 Cluster Root,则所在的 Cluster 将被解组
- 对不进行解组的 Cluster 执行引用更新操作