게임 엔진/Unreal

[Unreal] 메모리 관리

겜도리도리 2024. 1. 19. 11:25
반응형

앞서

이 포스팅은 이득우의 언리얼 프로그래밍 Part1을 수강하고 작성한 내용입니다.

 

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

C++은 메모리 주소에 직접 접근하는 포인터를 사용해 오브젝트를 관리한다.

메모리 누수 : new와 delete의 짝을 맞추지 않아 힙에 메모리가 남아 있는 경우

댕글링 포인터 : 이미 해제해서 무효화가 된 오브젝트의 주소를 포인터가 가리키는 경우

와일드 포인터 : 포인터의 값이 초기화되지 않은 경우

따라서 C++ 이후의 Java나 C#과 같은 언어에서는 포인터 대신 가비지 컬렉션 시스템을 도입했다.

 

가비지 컬렉션

프로그램에서 더 이상 사용하지 않는 오브젝트를 자동으로 감지해 메모리를 회수한다.

동적으로 생성된 모든 오브젝트 정보를 모아두고, 사용되지 않는 메모리를 추적한다.

 

Mark and Sweep

1. 저장소에서 최초 검색을 시작하는 루트 오브젝트를 표시한다.

2. 루트 오브젝트에서 참조하는 객체들을 다시 마크한다.

3. 마크된 객체로부터 다시 참조하는 객체를 찾아 마크하고 이를 반복한다.

4. 가비지 컬렉터가 저장소에서 마크되지 않은 객체들의 메모리를 해제한다. (Sweep)

 

언리얼의 가비지 컬렉션

언리얼엔진에서는 지정된 주기마다 가비지 컬렉션을 실행한다. 예제 프로젝트의 기본 값은 61.099998로 설정되어 있는 것을 프로젝트 셋팅에서 확인할 수 있다. 가비지 컬렉터의 작업이 부하가 작지는 않지만, 병렬 처리나 클러스터링을 활용해 성능 향상을 하고 있다. ForceGarabageCollection 함수로 회수를 재촉할 수도 있다.

 

가비지 컬렉션을 위해서 GUObjectArray라는 객체 저장소를 사용한다.

GUObjectArray는 모든 언리얼 오브젝트의 정보를 저장하는 전역 변수로 GUObjectArray의 각 요소에는 플래그가 설정되어 있다.

Garbage Flag : 다른 언리얼 오브젝트로부터 참조가 없어 회수 예정인 오브젝트

RootSet Flag : 다른 언리얼 오브젝트로부터 참조가 없어도 회수하지 않는 오브젝트

Garabage Flag 설정은 수동으로 설정해주는 것이 아니라, 시스템에서 자동으로 설정한다.

GUObjectArray에서 제공하는 AddToRoot 함수를 호출하면 오브젝트를 RootSet으로 설정해 줄 수 있고, 이 오브젝트는 메모리 회수로 부터 보호받게 된다. RemoveToRoot 함수를 호출하면 오브젝트를 RootSet을 제거해 줄 수 있다.

 

장점

앞서 언급되었던 C++ 메모리 관리의 문제점인 메모리 누수 문제, 댕글링 포인터 문제, 와일드 포인터 문제를 해결할 수 있다.

메모리 누수 : 가비지 컬렉터가 자동으로 회수한다.

댕글링 포인터 : isVaild 등을 통해 유효한지 유효하지 않은지 확인할 수 있다.

와일드 포인터 : 언리얼에서는 nullptr로 자동으로 초기화된다. (C++에서는 직접 nullptr로 초기화를 해줘야 한다.)

단, C++ 오브젝트는 직접 신경써야 하는데, 스마트 포인터 라이브러리등을 활용해서 해결할 수도 있다.

 

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

언리얼 엔진이 제공하는 방식으로 언리얼 오브젝트 참조하게 되면, 참조 당하는 오브젝트는 회수를 당하지 않는다.

가장 많이 사용하는 방식은 UPROPERTY 매크로를 선언하는 것이다.

UPROPERTY 매크로를 사용할 수 없는 상황에서는 AddReferencedObject 함수를 사용할 수도 있다.

 

일반 클래스에서 언리얼 오브젝트 관리하기

UPROPERTY 매크로를 사용할 수 없는 일반 C++ 클래스가 언리얼 오브젝트를 관리해야 하는 경우에는 FGCObject를 상속받은 뒤, AddReferencedObjects 함수를 구현하여 관리해줄 수 있다.

 

예제

1. 일단 가비지 컬렉션 사이클을 3초로 줄인다.

2. GameInstance에서 Init()을 오버라이드해 어플리케이션 초기화시 언리얼 오브젝트를 생성한다.

3. 3초가 지나면 가비지 컬렉션이 작동할 것이다.

4. Shudown()을 오버라이드 해 언리얼 오브젝트의 유효성을 확인한다.

 

GameInstance에서 UPROPERTY가 있고 없는 Student 객체 두 개를 선언한다.

 

그리고 Init()함수에서 NewObject로 UStudent 2개를 생성한다.

 

Shutdown에서는 오브젝트가 null인지, 유효한지 검사한다.

 

결과

 

UPROPERTY를 붙여주지 않는 NonPropStudent는 nullptr이 아니지만 유효하지 않다고 로그가 나오고

UPROPERTY를 붙여준 PropStudent는 nullptr이 아니고 유효하다고 로그가 나온다.

따라서 nullptr인지 아닌지만 체크해서는 안되고 UProperty를 붙여줘야 dangling 포인터 문제에서 벗어날 수 있다.

 

다른 예시

자료 구조 컨데이너안의 언리얼 오브젝트와 일반 C++ 클래스의 언리얼 오브젝트에 대해서도 알아보자.

NonPropStudents와 PropStudnts는 UStudent를 요소로 가지는 TArray 자료구조이다.

FStudentManager는 Ustudent를 멤버 변수로 가지는 C++ 클래스이다.

FStudentManager는 일반 C++ 객체이고 안의 UStudent는 언리얼 오브젝트기 때문에 FStudentManager는 UStudent 멤버를 관리할 능력이 없다. 또한 FStudentManager는 일반 C++ 객체기 때문에 멤버에 UPROPERTY()를 사용할 수도 없다.

따라서 UStudent에 nullptr을 기본값으로 설정해주고, FGCObject를 상속받아서 UStudent를 관리할 수 있도록 해줘야 한다.

FGCObject를 상속받았다면 AddReferencedObjects 함수와 GetReferencerName()을 구현해줘야 한다.

 

UMyGameInstance에서는 TArray 자료구조를 UPROPERTY가 있고 없는 버전의 2개를 생성하고

일반 C++클래스인 StudentManager를 UStudent를 인자로 넣어 생성한다.

 

결과

TArray에서도 UPROPERTY를 붙이지 않은 NonPropStudents는 nullptr이 아니지만, Invalid로 나온다.

따라서 TArray에서도 UROPERTY를 붙여줘야함을 알 수 있다.

 

StudentManager 클래스 FGCObject를 상속받았기 때문에 메모리 관리를 받을 수 있다.

StudentInManager에 있는 UStudent가 nullptr이 아니고, 유효하다고 로그가 찍히는 걸 확인할 수 있다.

FGCObject를 상속받지 않으면 앞서 잘못된 예시들과 비슷하게 nullptr이 아니지만 유효하지 않다고 나온다. 따라서 일반 C++ 객체에서 UObject를 관리해야할 때는 FGCObject를 상속받아 관리해줘야 한다.

반응형

'게임 엔진 > Unreal' 카테고리의 다른 글

[Unreal] ESlateVisibility  (0) 2024.03.05
[Unreal] 캐릭터와 입력 시스템  (0) 2024.02.25
[Unreal] 델리게이트  (0) 2024.01.15
[Unreal] 언리얼 컴포지션과 UENUM  (0) 2024.01.12
[Unreal] 인터페이스  (0) 2023.12.27