UE4 GC机制解析(四):Cluster(簇)

生命周期一致

Posted by bbkgl on August 28, 2021

远远围墙,隐隐茅堂。

飏青旗、流水桥旁。

Cluster中文叫“簇”,统一管理UE中将多个生命周期相同的UObject,以优化GC性能。

Cluster的作用和原理

区别于其他编程语言中普适性的GC,UE作为游戏引擎,其GC机制也与游戏息息相关。比如UE结合游戏分帧执行的特性,GC机制使用了分帧清理。同时游戏中也存在一些生死相关的物体,在GC标记时可以一起被标记为可达或不可达。

在游戏开发中,有一些生命周期相同的物体,如果按照常规的可达性分析逻辑进行遍历的话,同生共死的父物体和子物体会一起被遍历。

基本的结构如下图:

1630421649251

显然,这些生命周期一致的物体可以一同被标记为可达/不可达,而不是继续深搜/广搜标记,这样可以有效减少GC标记阶段的耗时。如下图中深色的对象(O2-O8),其生命周期一致,可一同被标记为可达。

1630508018589

在 UE 中就使用了一种结构对生命周期一致的物体进行组织,这种结构叫做Cluster。当然,Cluster并不能非常广泛的用在所有的 UObject 上,适用 Cluster 的有粒子系统,蓝图节点等。

Cluster的结构和存储

Cluster相关的设置在Project Settings中。

image-20210922154513283

结构

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 属于当前 Cluster
  • FUObjectCluster::ReferencedClusters 记录当前 Cluster 引用的其他 Cluster 的下标
  • FUObjectCluster::ReferencedByClusters 记录当前 Cluster 被哪些 Cluster 引用,应该是用于实现向上递归
  • FUObjectCluster::MutableObjects 记录被当前 Cluster 引用但不属于当前 Cluster 的 UObject
  • FUObjectCluster::bNeedsDissolving 则表示该 Cluster 是否应该被解组,在GC过程中被读写

UObjectArray.h 中会声明一个 FUObjectClusterContainer 类的全局变量 GUObjectClusters,用于管理所有的Cluster,貌似就那一个对象,为什么不用单例呢?

image-20210922150901587

同时在 FUObjectItem 中有一个 ClusterRootIndex 整数成员记录自己属于哪个Cluster。

  • 如果该值为负数,则通过 ` -ClusterRootIndex - 1` 计算所属 Cluster 的下标
  • 如果该值为正数( > 1),则表示当前 UObject 为 Cluster Root,同时 ClusterRootIndex 表示对应 Cluster 的下标
  • 该值为0,该 UObject 不属于任何 Cluster

Cluster与UObject

这里有几个问题,什么样的 UObject 能被加入到 Cluster 中,又是什么样的 UObject 能作为 Cluster Root。

这个问题在类 UObjectBaseUtility 中可以得到部分答案。

image-20210922153346236

image-20210922153359714

从上图中可以看出,默认情况下,UObject 不能作为 Cluster Root,需要的类需要自行实现 CanBeClusterRoot() 虚函数。

同时 UObject 根据 Outer 来进行判断是否可以加入到 Cluster 中,否则默认可以。

有区别的是,AActor 以及子类则默认不加入到 Cluster 中,可以在编辑器中进行勾选。

image-20210922154322386

image-20210922164050601

image-20210922154336602

部分其他子类重写 CanBeInCluster() 成员函数来修改是否能加入到 Cluster 中。

image-20210922164300114

默认返回false的有:UMaterialParameterCollectionUMediaPlayerUMediaPlaylistUSoundBaseUSoundCueUSoundNode,也就是说这些类及子类不能直接加入到 Cluster中。

默认情况下,UObject不可以作为Cluster Root,子类可以重写 CanBeClusterRoot() 让自己可以作为 Cluster Root。

引擎中默认情况下可以作为 Cluster Root的类有:UMaterialUmiMaterialUParticleSystem

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));
	}
}

函数执行如下过程:

  1. 创建空的 Cluster,并将当前的 UObject 作为 Cluster Root
  2. 创建引用收集的对象 ReferenceCollector,并将当前 UObject 存入到 ObjectsToSerialize 中,调用 TFastReferenceCollector::CollectReference() 完成引用收集,此时所有被当前 UObject 引用到的 UObject 对象都会被收集到对应的Cluster中,具体可以查阅 HandleTokenStreamObjectReference() 函数,其实也就是可达性分析的过程。
  3. 如果新 Cluster 中成员的数量大于等于用户设定的值,则将新 Cluster 进行排序,然后加入到 Cluster 集合中
  4. 如果新 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 中,分别添加到 ActorsForGCClusterActors 中,然后调用 ULevelActorContainer::CreateCluster()真正创建 Cluster。

ULevelActorContainer::CreateCluster() 的实现与 UObjectBaseUtility::CreateCluster() 没有太大差别,不再赘述。

所以 Level 中 Actor 的GC其实需要根据开发者/用户配置来决定是否通过 Cluster 执行。类 ULevelActorContainer 就辅助 ULevel 对加入到 Cluster 中的UObject进行GC,其他的 Actor 仍然参与到普通的GC流程中。

加入Cluster

主要函数是 UObjectBaseUtility::AddToCluster(),也就是由 UObject 自身调用,同时参数决定是否要加入到 MutableObjects 中。

也就是分成两种情况:

  1. 不加入到 MutableObjects 中,这里会通过TFastReferenceCollector::CollectReference() 执行引用收集,将所有被当前 UObject 引用到的所有 UObject 都加入到 Cluster 中,这个过程和上述 Cluster 创建的过程非常相似,不再赘述。
  2. 加入到 MutableObjects 中,这里不是简单的将 UObject 对应在 GUObjectArray 中的 Index 插入到 MutableObjects 中就完事,同时也要保持相对有序,也就是说 MutableObjects 必须是严格递增的序列,且没有重复的 Index,应该是要利用单调栈的性质来做点事情。。。

image-20210922203406648

解组

解组的实现在 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 解组。

image-20210928203716043

在可达性分析阶段,进行引用收集之前,会先将 UObject 标记为不可达,这个过程中同时也进行了 Cluster 的更新,更新包括两方面。

  • Cluster 中有标记为 PendingKill 的 Cluster Root,则所在的 Cluster 将被解组
  • 对不进行解组的 Cluster 执行引用更新操作