futurelabunseen / B-JeonganLee

UNSEEN 2nd Term Learning and Project Repo.
5 stars 0 forks source link

12강: 언리얼 엔진의 메모리 관리 #45

Closed fkdl0048 closed 6 months ago

fkdl0048 commented 6 months ago

12강: 언리얼 엔진의 메모리 관리

언리얼 엔진의 자동 메모리 관리

C++ 언어 메모리 관리의 문제점

C++은 저수준으로 메모리 주소에 직접 접근하는 포인터를 사용해 오브젝트를 관리한다. 그러다보니 프로그래머가 직접 할당과 해지를 해야하는데, 이를 잘 지키지 못하는 경우 다양한 문제가 발생할 수 있다.

위와 같이 잘못된 포인터 값은 다양한 문제를 일으키며, 한 번의 실수로 프로그램을 종료시키는 치명적인 문제로 이어진다. 이는 게임의 규모가 커질수록 더 복잡해지고 실수할 확률 또한 증가한다.

따라서 이후에 나온 객체지향 언어인 JAVA/C#은 가비지콜렉션을 도입하여 이러한 문제를 해결했다.

가비지 컬렉션 시스템

프로그램이 사용하지 않는 오브젝트를 자동으로 감지해 메모리를 회수하는 시스템이다. 동적으로 생성된 모든 오브젝트 정보를 모아둔 저장소를 사용해 사용되지 않는 메모리를 추적한다.

언리얼 엔진의 가비지 컬렉션 시스템

언리얼 엔진은 마크-스윕 방식의 가비지컬렉션 시스템을 자체적으로 구축한다. 지정된 주기마다 몰아서 없애도록 설정되어 있는데, 기본 값은 60초로 설정되어 있다. 성능향상을 위해 병령 처리, 클러스터링과 같은 기능을 탑재했다.

유니티는 C#기반으로 .NET 가비지 컬렉션을 사용한다. 내부 구현은 똑같은 마크-스윕 방식이다.

가바지 컬렉션을 위한 객체 저장소

관리되는 모든 언리얼 오브젝트의 정보를 저장하는 전역 변수 GUObjectArray가 있다. 각 요소에는 Flag가 설정되어 있으며 이를 통해 가비지 컬렉션의 대상 여부를 판단한다.

가비지 컬렉터는 GUObjectArray에 있는 플래그를 확인하여 빠르게 회수해야 할 오브젝트를 파악하고 메모리에서 제거함

가비지 컬렉터의 메모리 회수

가비지 컬렉터는 지정된 시간에 따라 주기적으로 메모리를 회수한다. Garbege플래그로 설정된 오브젝트를 파악하고 메모리를 안전하게 회수한다 이는 수동으로 설정하는 것이 아닌 시스템이 결정함

한 번 생성된 언리얼 오브젝트는 바로 삭제가 불가능함.

루트셋 플래그의 설정

만약에 런타임 내내 살아있어야 하는 중요한 언리얼 오브젝트가 있다면 RootSet플래그를 설정해야 한다. AddToRoot()함수를 사용하면 최초 탐색 목록으로 설정되며, 이는 메모리 회수로부터 보호받게 된다. RemoveFromRoot()함수를 사용하면 다시 일반 오브젝트로 돌아간다.

콘텐츠와 관련된 오브젝트에는 권장하지 않음.

언리얼 오브젝트를 통한 포인터 문제의 해결

회수되지 않는 언리얼 오브젝트

회수되지 않는 언리얼 오브젝트들은 언리얼 엔진 방식으로 참조를 설정한 언리얼 오브젝트이다. 먼저 UPROPERTY로 참조된 언리얼 오브젝트, AddReferencedObject람수를 통해 참조를 설정한 언리얼 오브젝트, AddToRoot함수로 루트셋에 추가된 언리얼 오브젝트 등이 있다.

정리하자면 언리얼 오브젝트 선언의 기본 원칙은 오브젝트 포인터는 가급적 UPROPERTY로 선언하고, 메모리는 가비지컬렉터가 자동으로 관리하도록 위임한다.

그렇다면 C++의 장점이자 단점인 메모리 접근을 통해 게임의 성능을 올려야 하는 부분에선 어떨까?

언리얼 오브젝트의 관리 원칙

가비지 컬렉션 테스트 환경 제작

실습

#pragma once

#include "CoreMinimal.h"
#include "Engine/GameInstance.h"
#include "MyGameInstance.generated.h"

UCLASS()
class UNREALMEMORY_API UMyGameInstance : public UGameInstance
{
 GENERATED_BODY()

public:
 virtual void Init() override;
 virtual void Shutdown() override;

private:
 TObjectPtr<class UStudent> NonPropStudent;

 UPROPERTY()
 TObjectPtr<class UStudent> PropStudent;

 TArray<TObjectPtr<class UStudent>> NonPropStudents;

 UPROPERTY()
 TArray<TObjectPtr<class UStudent>> PropStudents;

 // 일반 객체이기에 UPROPERTY()를 사용할 수 없다.
 // 따라서 값을 보장할 수 없기에 nullptr로 초기화한다.
 class FStudentManager* StudentManager = nullptr; 
};
#include "MyGameInstance.h"

#include "Student.h"
#include "StudentManager.h"

void CheckUObjectIsValid(const UObject* Object, const FString& InTag)
{
 if (Object->IsValidLowLevel())
 {
  UE_LOG(LogTemp, Log, TEXT("[%s] 유효한 언리얼 오브젝트"), *InTag);
 }
 else
 {
  UE_LOG(LogTemp, Log, TEXT("[%s] 유효하지 않은 언리얼 오브젝트"), *InTag);
 }
}

void CheckUObjectIsNull(const UObject* Object, const FString& InTag)
{
 if (nullptr == Object)
 {
  UE_LOG(LogTemp, Log, TEXT("[%s] 널 포인터 언리얼 오브젝트"), *InTag);
 }
 else
 {
  UE_LOG(LogTemp, Log, TEXT("[%s] 널 포인터가 아닌 언리얼 오브젝트"), *InTag);
 }
}

void UMyGameInstance::Init()
{
 Super::Init();

 NonPropStudent = NewObject<UStudent>();
 PropStudent = NewObject<UStudent>();

 NonPropStudents.Add(NewObject<UStudent>());
 PropStudents.Add(NewObject<UStudent>());

 StudentManager = new FStudentManager(NewObject<UStudent>());
}

void UMyGameInstance::Shutdown()
{
 Super::Shutdown();

 // 널 포인터가 아니지만 유효하지 않은 상태이다. 댕글링 발생
 CheckUObjectIsNull(NonPropStudent, TEXT("NonPropStudent"));
 CheckUObjectIsValid(NonPropStudent, TEXT("NonPropStudent"));

 // 널 포인터가 아니지만 유효한 상태이다. 문제없음
 CheckUObjectIsNull(PropStudent, TEXT("PropStudent"));
 CheckUObjectIsValid(PropStudent, TEXT("PropStudent"));

 // ----------------------------------------------
 // TArray (컨테이너 안의 언리얼 오브젝트)
 // ----------------------------------------------

 // 널 포인터가 아니지만 유효하지 않은 상태이다. 댕글링 발생
 CheckUObjectIsNull(NonPropStudents[0], TEXT("NonPropStudents"));
 CheckUObjectIsValid(NonPropStudents[0], TEXT("NonPropStudents"));

 // 널 포인터가 아니지만 유효한 상태이다. 문제없음
 CheckUObjectIsNull(PropStudents[0], TEXT("PropStudents"));
 CheckUObjectIsValid(PropStudents[0], TEXT("PropStudents"));

 // ----------------------------------------------
 // 일반 객체 (일반 객체 안의 언리얼 오브젝트)
 // ----------------------------------------------

 const UStudent* StudentInManager = StudentManager->GetStudent();

 delete StudentManager;
 StudentManager = nullptr;

 // 마찬가지로 댕글링 포인터 문제 발생
 // but FGCObject를 상속받은 클래스에서 AddReferencedObjects를 구현하면 문제가 해결된다.
 CheckUObjectIsNull(StudentInManager, TEXT("StudentInManager"));
 CheckUObjectIsValid(StudentInManager, TEXT("StudentInManager"));
}
#pragma once

#include "CoreMinimal.h"

class UNREALMEMORY_API FStudentManager : public FGCObject
{
public:
 FStudentManager(class UStudent* InStudent) : SafeStudent(InStudent) {}

 virtual void AddReferencedObjects(FReferenceCollector& Collector) override;
 virtual FString GetReferencerName() const override
 {
  return TEXT("FStudentManager");
 }

 const class UStudent* GetStudent() const { return SafeStudent; }
private:
 class UStudent* SafeStudent = nullptr;
};
#include "StudentManager.h"

#include "Student.h"

void FStudentManager::AddReferencedObjects(FReferenceCollector& Collector)
{
 if (SafeStudent->IsValidLowLevel())
 {
  Collector.AddReferencedObject(SafeStudent);
 }
}

정리