Taene's
[U C++] 언리얼 엔진의 자동 메모리 관리 시스템 본문
C++ 언어 메모리 관리의 문제점
- C++은 저수준으로 메모리 주소에 직접 접근하는 포인터를 사용해 오브젝트를 관리한다.
- 그러다보니 프로그래머가 직접 할당(new)과 해지(delete)를 짝 맞춰야 한다.
- 이를 잘 지키지 못할 경우 다양한 문제가 발생한다.
- 잘못된 포인터 사용 예시
- 메모리 누수(Leak): new를 했는데 delete 짝을 맞추지 못해 힙에 메모리가 그대로 남아있는 경우
- 허상(Dangling) 포인터: (다른곳에서) 이미 해제해 무효화된 오브젝트의 주소를 가리키는 포인터
- 와일드(Wild) 포인터: 값이 초기화되지 않아 엉뚱한 주소를 가리키는 포인터
- 잘못된 포인터 값은 다양한 문제를 일으키며, 한 번의 실수가 프로그램을 종료시킨다.
- 게임 규모가 커지고 구조가 복잡해질수록 프로그래머가 실수할 확률은 크게 증가한다.
- C++ 이후에 나온 언어(Java/C#)들은 이런 고질적인 문제를 해결하기 위해 포인터를 버리는 대신 가비지 컬렉션 시스템을 도입했다.
가비지 컬렉션 시스템
- 프로그램에서 더 이상 사용하지 않는 오브젝트를 자동으로 감지해 메모리를 회수하는 시스템이다.
- 동적으로 생성된 모든 오브젝트 정보를 모아둔 저장소를 사용해 사용되지 않는 메모리를 추적한다.
- 마크-스윕(Mark-Sweep) 방식의 가비지 컬렉션
- 저장소에서 최초로 검색을 시작하는 루트 오브젝트를 표기한다.
- 루트 오브젝트가 참조하는 객체를 찾아 (사용하는지 여부를 파악하고 사용하면) 마크(Mark)한다.
- 마크된 객체로부터 다시 참조하는 객체를 찾아 마크하고 이를 계속 반복한다.
- 이제 저장소에는 마크된 객체와 마크되지 않은 객체의 두 그룹으로 나뉜다.
- 가비지 컬렉터가 저장소에서 마크되지 않은 객체(가비지)들의 메모리를 한번에 회수한다.(Sweep)
언리얼 엔진의 가비지 컬렉션 시스템
- 마크-스윕 방식의 가비지 컬렉션 시스템을 자체적으로 구축했다.
- 지정된 주기마다 몰아서 없애도록 설정되어 있다.( GCCycle, 기본 값 60초 )
- 성능 향상을 위해 병렬 처리와 클러스터링 같은 기능을 탑재했다.
언리얼 엔진의 가비지 컬렉션을 위한 객체 저장소
- 관리되는 모든 언리얼 오브젝트의 정보를 저장하는 전역 변수: GUObjectArray
- GUObjectArray의 각 요소에는 플래그(Flag)가 설정되어 있다.
- 가비지 컬렉터가 참고하는 주요 플래그
- Garbage 플래그: 다른 언리얼 오브젝트로부터의 참조가 없어 회수 예정인 오브젝트
- RootSet 플래그: 다른 언리얼 오브젝트로부터 참조가 없어도 회수하지 않는 특별한 오브젝트
언리얼엔진의 가비지 컬렉터의 메모리 회수
- 가비지 컬렉터는 지정된 시간에 따라 주기적으로 메모리를 회수한다.( GCCycle, 기본 값 60초 )
- Garbage 플래그로 설정된 오브젝트를 파악하고 메모리를 안전하게 회수한다.
- Garbage 플래그는 수동으로 설정하는 것이 아니라 시스템이 알아서 설정한다.
- 한 번 생성된 언리얼 오브젝트는 바로 삭제가 불가능하다.
- 한 번 생성된 언리얼 오브젝트를 삭제하기 위해선 C++의 delete 키워드를 사용하는 것이 아니라 레퍼런스 정보를 없앰으로써 언리얼의 가비지 컬렉터가 자동으로 메모리를 회수하도록 설정하는 것이다.
RootSet 플래그의 설정
- AddToRoot 함수를 호출해 루트셋 플래그를 설정하면 최초 탐색 목록으로 설정된다.
- 루트셋으로 설정된 언리얼 오브젝트는 메모리 회수로부터 보호받는다.
- RemoveFromRoot 함수를 호출해 루트셋 플래그를 제거할 수 있다.
- 콘텐츠 관련 오브젝트에 루트셋을 설정하는 방법은 권장하지 않는다.
언리얼 오브젝트를 통한 포인터 문제의 해결
- 메모리 누수 문제
- 언리얼 오브젝트는 가비지 컬렉터를 통해 자동으로 해결한다.
- C++ 오브젝트는 직접 신경써야 한다.( 스마트 포인터 라이브러리의 활용 )
- 댕글링 포인터 문제
- 언리얼 오브젝트는 이를 탐지하기 위한 함수를 제공한다.( ::IsValid())
- C++ 오브젝트는 직접 신경써야 한다.( 스마트 포인터 라이브러리의 활용 )
- 와일드 포인터 문제
- 언리얼 오브젝트에 UPROPERTY 속성을 지정하면 자동으로 nullptr로 초기화 해준다.
- C++ 오브젝트의 포인터는 직접 nullptr로 초기화해야 한다.( 또는 스마트 포인터 라이브러리를 활용 )
회수되지 않는 언리얼 오브젝트
- 언리얼 엔진 방식으로 참조를 설정한 언리얼 오브젝트
- UPROPERTY로 참조된 언리얼 오브젝트( 대부분의 경우 이를 사용 )
- AddReferencedObject 함수를 통해 참조를 설정한 언리얼 오브젝트
- 루트셋(RootSet)으로 지정된 언리얼 오브젝트 ( 굉장히 중요하게 취급하는 오브젝트 )
- 오브젝트 선언의 기본 원칙 - 오브젝트 포인터는 가급적 UPROPERTY로 선언하고, 메모리는 가비지 컬렉터가 자동으로 관리하도록 위임한다.
일반 클래스에서 언리얼 오브젝트를 관리하는 경우
- UPROPERTY를 사용하지 못하는 일반 C++ 클래스가 언리얼 오브젝트를 관리해야 하는 경우
- FGCObject 클래스를 상속받은 후 AddReferencedObjects 함수를 구현한다.
- 함수 구현 부에서 관리할 언리얼 오브젝트를 추가해준다.
class UNREALMEMORY_API FStudentManager : public FGCObject
{
public:
FStudentManager(class UStudent* InStudent);
~FStudentManager();
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;
};
언리얼 오브젝트의 관리 원칙
- 생성된 언리얼 오브젝트를 유지하기 위해 레퍼런스 참조 방법을 설계할 것
- 언리얼 오브젝트 내의 언리얼 오브젝트: UPROPERTY 사용
- 일반 C++ 오브젝트 내의 언리얼 오브젝트: FGCObject의 상속 후 AddReferencedObjects 함수 구현
- 생성된 언리얼 오브젝트는 강제로 지우려 하지 말 것
- 참조를 끊는다는 생각으로 설계할 것
- 가비지 컬렉터에게 회수를 재촉할 수는 있다.( ForceGarbageCollection 함수 )
- 콘텐츠 제작에서 Actor 오브젝트를 소멸하기 위한 Destroy 함수를 사용할 수 있으나, 내부 동작은 동일하다.( 가비지 컬렉터에 위임 )
언리얼 메모리 관리 시스템(가비지 컬렉션 시스템)의 활용
// MyGameInstance.h
#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;
// 일반 C++ 클래스에서 언리얼 오브젝트를 어떻게 관리하는가 ?
class FStudentManager* StudentManager = nullptr; // 일반 객체이기 때문에 UPROPERTY()를 사용할 수 없다.
};
// MyGameInstance.cpp
#include "MyGameInstance.h"
#include "Student.h"
#include "StudentManager.h"
void CheckUObjectIsValid(const UObject* InObject, const FString& InTag) // 유효한지 아닌지 체크
{
// 인자로 받은 InObject가 유효한지 아닌지 판단하는 로직
// 일반적으로 콘텐츠 제작에서는 ::IsValid 함수를 많이 사용한다.
// 보다 정교하게 체크해줄수 있는 함수
if (InObject->IsValidLowLevel())
{
UE_LOG(LogTemp, Log, TEXT("[%s] 유효한 언리얼 오브젝트"), *InTag);
}
else
{
UE_LOG(LogTemp, Log, TEXT("[%s] 유효하지 않은 언리얼 오브젝트"), *InTag);
}
}
void CheckUObjectIsNull(const UObject* InObject, const FString& InTag) // null값인지 아닌지
{
if (InObject == nullptr)
{
UE_LOG(LogTemp, Log, TEXT("[%s] nullptr 언리얼 오브젝트"), *InTag);
}
else
{
UE_LOG(LogTemp, Log, TEXT("[%s] nullptr이 아닌 언리얼 오브젝트"), *InTag);
}
}
void UMyGameInstance::Init()
{
Super::Init();
NonPropStudent = NewObject<UStudent>();
PropStudent = NewObject<UStudent>();
NonPropStudents.Add(NewObject<UStudent>());
PropStudents.Add(NewObject<UStudent>());
// 일반 객체 StudentManager의 생성
StudentManager = new FStudentManager(NewObject<UStudent>());
}
void UMyGameInstance::Shutdown()
{
Super::Shutdown();
// ===일반 C++ 클래스에서 언리얼 오브젝트를 어떻게 관리하는가 ?===
const UStudent* StudentInManager = StudentManager->GetStudent();
// 일반 객체 StudentManager의 소멸
delete StudentManager;
StudentManager = nullptr;
CheckUObjectIsNull(StudentInManager, TEXT("StudentInManager"));
CheckUObjectIsValid(StudentInManager, TEXT("StudentInManager"));
/*
* FGCObject 상속 전 실행결과
* LogTemp: [StudentInManager] nullptr이 아닌 언리얼 오브젝트
* LogTemp: [StudentInManager] 유효하지 않은 언리얼 오브젝트
*/
/*
* FGCObject 상속 후 실행결과
* LogTemp: [StudentInManager] nullptr이 아닌 언리얼 오브젝트
* LogTemp: [StudentInManager] 유효한 언리얼 오브젝트
*/
// ================================================
// ========언리얼 오브젝트를 어떻게 관리하는가 ?========
CheckUObjectIsNull(NonPropStudent, TEXT("NonPropStudent"));
CheckUObjectIsValid(NonPropStudent, TEXT("NonPropStudent"));
CheckUObjectIsNull(PropStudent, TEXT("PropStudent"));
CheckUObjectIsValid(PropStudent, TEXT("PropStudent"));
/*
* 실행결과
* LogTemp: [NonPropStudent] nullptr이 아닌 언리얼 오브젝트
* LogTemp: [NonPropStudent] 유효하지 않은 언리얼 오브젝트
* LogTemp : [PropStudent] nullptr이 아닌 언리얼 오브젝트
* LogTemp : [PropStudent] 유효한 언리얼 오브젝트
*/
// ================================================
// ====TArray 언리얼 오브젝트를 어떻게 관리하는가 ?====
CheckUObjectIsNull(NonPropStudents[0], TEXT("NonPropStudents"));
CheckUObjectIsValid(NonPropStudents[0], TEXT("NonPropStudents"));
CheckUObjectIsNull(PropStudents[0], TEXT("PropStudents"));
CheckUObjectIsValid(PropStudents[0], TEXT("PropStudents"));
/*
* 실행결과
* LogTemp: [NonPropStudents] nullptr이 아닌 언리얼 오브젝트
* LogTemp: [NonPropStudents] 유효하지 않은 언리얼 오브젝트
* LogTemp: [PropStudents] nullptr이 아닌 언리얼 오브젝트
* LogTemp: [PropStudents] 유효한 언리얼 오브젝트
*/
// ================================================
}
// StudentManager.h
#pragma once
#include "CoreMinimal.h"
/**
*
*/
class UNREALMEMORY_API FStudentManager : public FGCObject // 일반 C++ 오브젝트는 F접두사를 붙인다.(파일 이름은 그대로 놔둠)
{
public:
FStudentManager(class UStudent* InStudent) : SafeStudent(InStudent) {}
// FGCObject를 상속받아서 아래 두 함수를 구현해줘야 일반 C++ 클래스에서 언리얼 오브젝트를 관리할 수 있다.
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; // 일반 C++ 클래스이기 때문에 TObjectPtr없이 사용
};
// StudentManager.cpp
#include "StudentManager.h"
#include "Student.h"
void FStudentManager::AddReferencedObjects(FReferenceCollector& Collector)
{
if (SafeStudent->IsValidLowLevel())
{
Collector.AddReferencedObject(SafeStudent);
}
}
'Unreal5 > Unreal C++' 카테고리의 다른 글
[U C++] 언리얼 오브젝트 패키지(Package)와 애셋(Asset) (0) | 2024.07.12 |
---|---|
[U C++] 직렬화(Serialization) (1) | 2024.07.12 |
[U C++] UCL - TMap (0) | 2024.07.10 |
[U C++] 구조체 UStructs (0) | 2024.07.10 |
[U C++] UCL - TSet (0) | 2024.07.09 |