KoreanFoodie's Study

언리얼 가비지 컬렉션 (Unreal Garbage Collection) 본문

Game Dev/Unreal C++ : Study

언리얼 가비지 컬렉션 (Unreal Garbage Collection)

GoldGiver 2022. 4. 1. 14:49

가비지 컬렉션

언리얼에서  UProperty 를 붙인 객체는 언리얼 엔진이 자동으로 가비지 컬렉터를 이용해 메모리를 관리한다. 가비지 컬렉션을 수행함에 있어 리플렉션 시스템을 사용하는데, 엔진이 객체와 속성값을 알고 있으므로, 더 이상 사용되지 않아 삭제해도 괜찮은 객체들을 구분할 수 있기 때문이다.

언리얼 엔진에서는 Reference Graph 를 만들어 오브젝트들의 사용 여부를 구분한다. 이 그래프 루트에는 "Root Set" 이라 지정된 오브젝트 셋이 존재하며, "Root Set" 에 포함된 객체들은 가비지 컬렉션 대상에서 제외된다. UObject::BaseUtility::AddToRoot 함수를 이용하면 객체를 "Root Set" 에 추가시킬 수 있다. ( 예시 : UMyObject->AddToRoot() )

가비지 컬렉션이 실행되면 엔진은 "Root Set" 을 시작으로 UObject 레퍼런스 트리를 검색해 참조된 오브젝트를 모두 추적한다. 이 검색 과정에서 찾지 못한 것들은 더 이상 필요하지 않는 오브젝트라고 판단하고 제거할 수 있는 것이다. 이는 가비지 컬렉션이 리플렉션 데이터에 의존하므로 가능한 일이다.

 

다만 엔진이 가비지 컬렉터를 제대로 수행하게 만들기 위해서는 다음과 같은 규칙들을 지켜줘야 한다. 

 

1) UPROPERTY 선언

클래스 내부 멤버 변수가 클래스의 객체의 수명과 운명을 함께한다면 UPROPERTY 로 선언해야 한다. 이는 수명 주기가 같다는 뜻이다. 반면 잠시 사용할 UObject 나 일반 클래스 객체들은 별도로 관리해 주어야 한다.

2) 멤버가 가리키는 포인터

엔진이 인식하거나 관리하지 않는 메모리 영역을 가리키도록 만들면 안된다. 따라서 멤버가 가리키는 포인터는 UObject 또는 그 자식들로 한정시키는 것이 좋다.

3) TArray 를 활용하자

UObject 또는 자식들에 대한 포인터를 안전하게 담을 수 있는 컨테이너는 TArray 가 유일하다. 

 

예시 코드를 한 번 보자.

UCLASS()
class AMyActor : public AActor
{
    GENERATED_BODY()

public:
    UPROPERTY()
    MyGCType* SafeObject;

    MyGCType* DoomedObject;

    AMyActor(const FObjectInitializer& ObjectInitializer)
        : Super(ObjectInitializer)
    {
        SafeObject = NewObject<MyGCType>();
        DoomedObject = NewObject<MyGCType>();
    }
};

void SpawnMyActor(UWorld* World, FVector Location, FRotator Rotation)
{
    World->SpawnActor<AMyActor>(Location, Rotation);
}

위에서, SafeObject 는 UPROPERTY( ) 선언이 되어 있으므로, root set object 로부터 참조가 가능해 가비지 컬렉션 대상이 되지 않는다. 하지만 DoomedObject 는 root set object 로부터 참조가 되지 않아 가비지 컬렉팅이 되고, 결국 dangling pointer 로 남아 있을 수 있다.

특정 UObject 가 가비지 컬렉션되면, 모든 UPROPERTY reference 가 null 로 세팅된다. 이는 오브젝트가 가비지 컬렉션되었는지 아닌지를 안전하게 검증할 수 있게 만들어 준다.

if (MyActor->SafeObject != nullptr)
{
	// Use SafeObject
}

위에서 MyActor->SafeObject 가 nullptr 가 아니라는 뜻은 해당 UObject 가 가비지 컬렉팅을 기다리고 있지 않다는 뜻이다. 액터를 Destroy 하게 되면, 가비지 컬렉터가 다시 실행되기 전까지는 실제로 제거되지 않으므로, IsPendingKill 메소드를 사용하여 해당 UObject 가 수거를 기다리고 있는지 체크할 수 있다. 

 

 

UStructs vs UObjects

구조체는 "value" 타입으로 사용하기 위한 것이다. 구조체는 가비지 컬렉션의 대상이 아니므로, UObject 내에 위치해야 메모리가 올바르게 회수될 수 있다.

UStruct 의 장점은 크기가 매우 작다는 것인데, UObject 는 데이터 외에도 book-keeping 데이터를 가져야 하지만, UStruct (기술적으로 UScriptStruct) 는 사용자가 입력한 크기만큼만 사용하기 때문이다. 반면, UStruct 는 다른 객체의 멤버 구조체를 직접 가리키는 것이 안전하지 않다는 단점 또한 존재한다.

UObject 는 UStruct 에 비해 무겁지만 일반적으로 안전하게 포인팅될 수 있다. 

 

 

가비지 컬렉션 요청

UObject 들은 가비지 컬렉션을 명시적으로 요청할 수 있다. 해당 함수들을 호출하면, 가비지 컬렉션 수행 대상으로 등록되게 된다.

1) UObejct

UObejct::ConditionalBeginDestroy()

2) AActor

AActor::DestroyActor()

 

 

강제로 가비지 컬랙션 하기

World::ForceGarbageCollection(bool bFullPurge) 함수를 통해 강제로 가비지 컬렉션을 수행할 수 있다. 

 

 

참고 자료 :

Unreal Object Handling

액터의 수명 주기

TArray : 언리얼 엔진의 배열

Comments