[UE5][GC] GUObjectArray, TWeakObjectPtr

ChangJin·2025년 5월 27일
0

Unreal Engine5

목록 보기
122/122
post-thumbnail

글에 사용된 모든 그림과 내용은 직접 작성한 것입니다.


글의 목적

TWeakObjectPtr를 사용하면서 작동 원리를 정리하기 위함. 또한 언리얼의 GC가 어떻게 작동하는지 간단하게 알아보고 TWeakObjectPtr의 사용에 있어서 GUObjectArray가 어떻게 쓰이는지 정리하기 위함.


알게 된 점

  1. TWeakObjectPtr는 해시 값으로 SerialNumber를 저장하고 그 SerialNumber를 가지고 안전하게 객체에 접근한다.
  2. GUObjectArray는 생성된 모든 UObject 전체를 보관하는 전역 객체이다. 이를 Free list로 관리한다.
  3. FUObjectItem안에 있는 SerialNumber를 실제로 더하고 빼어 Free list에 추가한다.


언리얼에서의 TWeakObjectPtr

C++의 스마트 포인터인 weak_ptr과 사용법은 같습니다. 언리얼에서는 이를 TWeakObjectPtr로 새롭게 만들어 두었습니다. C++의 weak_ptr은 shared_ptr로 만들어진 객체에만 사용할 수 있고, 참조 카운터를 사용해 안전하게 접근합니다. 언리얼의 TWeakObjectPtr는 UObject 기반의 모든 객체를 대상으로 하고 GC를 지원합니다.

다음처럼 해시값으로 SerialNumber를 저장하고 Get()을 했을 때 GUObjectArray에서 직접 인덱스를 사용해 UObject* 조회한 후 가져오게 됩니다.


	/**
	 * Returns true if two weak pointers were originally set to the same object, even if they are now stale
	 * @param Other weak pointer to compare to
	 */
FORCEINLINE bool HasSameIndexAndSerialNumber(const TWeakObjectPtr& Other) const
{
	return static_cast<const TWeakObjectPtrBase&>(*this).HasSameIndexAndSerialNumber(static_cast<const TWeakObjectPtrBase&>(Other));
}
    
/**
 * SetKeyFuncs for TWeakObjectPtrs which allow the key to become stale without invalidating the set.
 */
template <typename ElementType, bool bInAllowDuplicateKeys = false>
struct TWeakObjectPtrSetKeyFuncs : DefaultKeyFuncs<ElementType, bInAllowDuplicateKeys>
{
	typedef typename DefaultKeyFuncs<ElementType, bInAllowDuplicateKeys>::KeyInitType KeyInitType;

	static FORCEINLINE bool Matches(KeyInitType A, KeyInitType B)
	{
		return A.HasSameIndexAndSerialNumber(B);
	}

	static FORCEINLINE uint32 GetKeyHash(KeyInitType Key)
	{
		return GetTypeHash(Key);
	}
};


FORCEINLINE bool IsValid(bool bEvenIfPendingKill, bool bThreadsafeTest = false) const
{
	return TWeakObjectPtrBase::IsValid(bEvenIfPendingKill, bThreadsafeTest);
}


FWeakObjectPtr의 Index / SerialNumber는 어떻게 동기화될까?

FWeakObjectPtr은 FUObjectItem의 다음 두 값을 내부적으로 저장합니다 (해시를 통해)

int32 ObjectIndex;
int32 ObjectSerialNumber;

ObjectIndex: GUObjectArray에서 UObject를 가리키는 인덱스
ObjectSerialNumber: 해당 인덱스의 UObject가 살아있는지를 확인하는 일종의 버전 번호

struct
#if !STATS && !ENABLE_STATNAMEDEVENTS_UOBJECT
	// Packing avoids 20% mem waste and improves perf
	GCC_PACK(4)
#endif
	FUObjectItem
{
	friend class FUObjectArray;
	friend class UE::GC::Private::FGCFlags;

	// Pointer to the allocated object
	class UObjectBase* Object;
private:
	// Internal flags. These can only be changed via Set* and Clear* functions
	int32 Flags;
public:
	// UObject Owner Cluster Index
	int32 ClusterRootIndex;	
	// Weak Object Pointer Serial number associated with the object
	int32 SerialNumber;
	// RefCount associated with the object preventing its destruction.
	int32 RefCount;

#if STATS || ENABLE_STATNAMEDEVENTS_UOBJECT
	/** Stat id of this object, 0 if nobody asked for it yet */
	mutable TStatId StatID;
  • 객체 생성 시

GUObjectArray에 빈 슬롯(해방된 인덱스)이 있으면 재사용합니다. 없다면 그 슬롯에 새로운 UObject가 생성됩니다. 새로운 슬롯에 UObject가 생성됐다면 SerialNumber가 증가합니다. 이전에 동일 인덱스를 참조하던 FWeakObjectPtr은 더 이상 유효하지 않습니다.

참고
GUObjectArray는 UObject 전체를 보관하는 전역 객체입니다. 내부적으로는 FUObjectItem 배열을 관리합니다. 각 UObject는 생성 시 ObjectIndex를 할당받고, 소멸 시 해당 인덱스는 "free list"로 들어갑니다. 새로 생성된 객체는 해당 인덱스를 재사용하되, SerialNumber++로 이전 포인터를 무효화합니다.

  • 객체 소멸 시

UObject는 GC에 의해 소멸되면 GUObjectArray.RemoveObject() 호출합니다.

/**
 * Removes an object from delete listeners
 *
 * @param Object to remove from delete listeners
 */
void FUObjectArray::RemoveObjectFromDeleteListeners(UObjectBase* Object)
{
#if THREADSAFE_UOBJECTS
	FTransactionallySafeScopeLock UObjectDeleteListenersLock(&UObjectDeleteListenersCritical);
#endif
	int32 Index = Object->InternalIndex;
	check(Index >= 0);
	// Iterate in reverse order so that when one of the listeners removes itself from the array inside of NotifyUObjectDeleted we don't skip the next listener.
	for (int32 ListenerIndex = UObjectDeleteListeners.Num() - 1; ListenerIndex >= 0; --ListenerIndex)
	{
		UObjectDeleteListeners[ListenerIndex]->NotifyUObjectDeleted(Object, Index);
	}
}

해당 인덱스를 "free list"에 추가합니다. 해당 인덱스는 다음 객체가 생성될 때 재사용됩니다. FWeakObjectPtr::Get() / IsValid() 호출 시에 GUObjectArray.GetObjectFromWeakPointer(ObjectIndex, SerialNumber) 내부에서 현재 인덱스의 UObject의 SerialNumber와 비교합니다. 만약 다르면 stale pointer로 간주합니다.

참고
stale pointer는 댕글링 포인터라고 생각하면 됩니다. stale pointer는 오래되어 더 이상 유효하지 않은 포인터라는 뉘앙스에 조금 더 초점이 맞추어져 있습니다. 댕글링 포인터는 이미 해제된 메모리를 가리키는 포인터입니다.

가비지 컬렉션(GC) 중에 UObject의 ConditionalFinishDestroy() 함수가 호출됩니다. 내부를 보면 GUObjectArray의 SerialNumber에 접근하고, GUObjectArray에 있는 자신을 RemoveObjectFromDeleteListeners() 매개변수로 전달합니다.

bool UObject::ConditionalFinishDestroy()
{
	check(IsValidLowLevel());
	if( !HasAnyFlags(RF_FinishDestroyed) )
	{
		SetFlags(RF_FinishDestroyed);
#if !(UE_BUILD_SHIPPING || UE_BUILD_TEST)
		checkSlow(!DebugFinishDestroyed.Contains(this));
		DebugFinishDestroyed.Add(this);
#endif
		FinishDestroy();

		// Make sure this object can't be accessed via weak pointers after it's been FinishDestroyed
		GUObjectArray.ResetSerialNumber(this);

		// Make sure this object can't be found through any delete listeners (annotation maps etc) after it's been FinishDestroyed
		GUObjectArray.RemoveObjectFromDeleteListeners(this);

#if !(UE_BUILD_SHIPPING || UE_BUILD_TEST)
		if( DebugFinishDestroyed.Contains(this) )
		{
			UE_LOG(LogObj, Fatal, TEXT("%s failed to route FinishDestroy"), *GetFullName() );
		}
#endif
		return true;
	}
	else 
	{
		return false;
	}
}

여기서 해당 인덱스를 free list에 추가해 재사용 가능 상태로 만듭니다.
즉 제일 중요한 곳인데, 이 시점에 SerialNumber가 증가되어 Stale의 판단 기준이 됩니다. 즉 weak_ptr의 참조 카운터가 1 증가하는 것을 떠올려보면 이해가 될 겁니다.

void FUObjectArray::FreeObjectIndex(int32 Index)
{
	FUObjectItem* ObjectItem = &ObjObjects[Index];
	ObjectItem->SerialNumber++; // 중요!
	ObjectItem->Object = nullptr;

	// Free list에 추가
	ObjFirstGCIndex--;
}

RemoveObjectFromDeleteListeners 함수에서는 FWeakObjectPtr을 포함한 모든 약한 참조자들에게 알림을 줍니다. 즉 지금의 상황을 모두 업데이트하는 거라고 생각하시면 됩니다.

void FUObjectArray::RemoveObjectFromDeleteListeners(UObjectBase* Object)
{
	int32 Index = Object->InternalIndex;
	for (int32 i = UObjectDeleteListeners.Num() - 1; i >= 0; --i)
	{
		UObjectDeleteListeners[i]->NotifyUObjectDeleted(Object, Index);
	}
}
	/**
	 * Returns the UObject corresponding to index. Be advised this is only for very low level use.
	 *
	 * @param Index index of object to return
	 * @return Object at this index
	 */
	FORCEINLINE FUObjectItem* IndexToObject(int32 Index)
	{
		check(Index >= 0);
		if (Index < ObjObjects.Num())
		{
			return const_cast<FUObjectItem*>(&ObjObjects[Index]);
		}
		return nullptr;
	}


profile
게임 프로그래머

0개의 댓글