언리얼/언리얼 C++

언리얼 엔진의 메모리 관리 (1) ~ 가비지 컬렉션 시스템

monstro 2024. 8. 3. 20:12
728x90
반응형

이전의 포스팅에서 언리얼 엔진은 C++을 사용함으로서

장점은 장점대로 취하면서, 단점은 현대 객체지향 언어의 특성을 사용하여 극복한다고 말씀드렸습니다.

 

특히, 포인터를 사용함으로써 발생하는 문제는 메모리에 직접적인 손상을 가할 수가 있기에 매우 중요하게 다뤄야 합니다.

포인터를 사용함으로써 발생하는 문제는 크게 다음의 3가지로 분류할 수 있습니다.

 

1) 메모리 누수 : delete가 되지 않아 heap에 메모리가 그대로 남겨짐 이로 인해 지속적인 자원 낭비 발생

2) 허상 포인터 : nullptr는 아니지만, delete된 오브젝트의 주소를 가리키는 포인터

3) 와일드 포인터 : 값이 초기화되지 않아 엉뚱한 주소를 가리키는 포인터

 

이번 포스팅에서는 언리얼 엔진이 현대적인 객체지향 언어의 특징, 그 중에서도 가비지 컬렉션을 통해

어떻게 이 문제에 대처하는지 알아보겠습니다.

 

1) 가비지 컬렉션이란?

가비지 컬렉션은

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

가비지 컬렉션을 지원하는 대표적인 언어로는 C#을 예로 들수 있습니다.

 

이런 가비지 컬렉션은 동적으로 생성된 오브젝트 정보를 모아둔 저장소를 사용해

더 이상 사용되지 않는 메모리를 추적하는 방식으로 동작합니다.

 

이런 방식을 Mark-Sweep 방식이라고 하는데, 언리얼 엔진의 가비지 컬렉션의 경우 이 방식을 사용합니다.

 

Mark-Sweep 방식의 경우, 다음의 과정으로 동작합니다.

1) 우선, 저장소에서 최초로 탐색할 루트 데이터를 선택합니다.

2) 그 후에 루트 데이터가 참조하는 데이터를 찾아 표시(Mark)합니다.

3) 표시된 데이터로부터 참조하는 데이터를 찾아 2~3의 과정을 반복합니다.

4) 이제 저장소에는 표시된 표시된 데이터와 그렇지 않은 데이터 두 개의 그룹이 존재합니다.

5) 가비지 컬렉터가 표시되지 않은 데이터 그룹의 메모리를 회수(Sweep)합니다.

 

2) 언리얼 엔진의 가비지 컬렉션

언리얼 엔진은 Mark-Sweep 방식의 가비지 컬렉션을 사용합니다.

위 방식에서 사용하는 저장소는 언리얼 엔진에서 GUObjectArray라는 전역변수로 접근이 가능합니다.

 

또한, 프로젝트의 설정을 통해 가비지 컬렉션의 과정이 일어나는 주기를 설계자가 조정이 가능합니다.

이를 위해서는 Project Settings->Engine/Garbage Collection에서 

Time Between Purging Pending Kill Objects의 값을 조절하여 조정할 수 있습니다.

 

조정 후에 에디터를 재시작하면 설정값이 프로젝트에 적용됩니다.

그러나 이 값을 너무 낮춰서 너무 빈번하게 Mark-Sweep이 이뤄지도록 설정하면

퍼포먼스가 떨어질 수 있다는 것을 명심해야 합니다.

 

3) 예제

이제 예제를 통해 언리얼 엔진의 가비지 컬렉션을 사용하는 경우의 이점을 직접 확인해보겠습니다.

이번 예제는 언리얼 오브젝트가 아닌 두 개의 C++ 오브젝트를 사용할 예정입니다.

 

하나는 일반적인 C++ 오브젝트이고,

다른 하나는 FGCObject라는 언리얼 오브젝트를 멤버로 갖는 C++ 오브젝트입니다.

 

UCLASS가 수식된 클래스의 인스턴스의 경우, 자동으로 가비지 컬렉션의 관리대상이 되므로

C++ 오브젝트를 사용하였음을 유의해주시길 바랍니다.

 

제일 먼저, Mark-Sweep되는 주기를 기본 60초에서 3초로 변경하였습니다.

코드의 구성은 다음과 같습니다.

 

 

일반 C++ 오브젝트인 NormalObject 클래스입니다.

다음은 FGCObject로부터 상속된 C++ 오브젝트인 InheritedObject 클래스입니다.

 

마지막으로, GameInstance입니다.

 

GameInstance에서는 두개의 함수를 통해 해당 오브젝트를 판단합니다.

 

Init()에서는 각각의 포인터가 주소를 담게끔 설계하였습니다.

Shutdown()에서는 각각의 포인터를 해제하여 오브젝트별로 관리가 어떻게 이뤄지는 지 확인합니다.

 

실핼결과는 다음과 같습니다.

 

결과를 보면 

일반 C++ 오브젝트와 UPROPERTY()가 수식되지 않은 언리얼 오브젝트의 포인터의 경우,

허상 포인터 문제가 발생하는 것을 알 수 있습니다.

 

그러나 FCGObject에서 상속된 C++ 오브젝트와 UPROPERTY()가 수식된 프로퍼티의 경우,

언리얼 엔진의 가비지 컬렉션에 의해 관리되어 문제없이 로그가 찍히는 것을 볼 수 있습니다.

 

이렇듯, 게임을 관리함에 있어서 특히 언리얼 오브젝트를 가리키는 포인터의 경우

최대한 UPROPERTY() 매크로를 사용하여 가비지 컬렉터에 관리를 맡기는 것이 매우 유용하고 안전합니다.

 

사람은 반드시 실수를 하기 때문에, 하나의 실수가 프로그램 전체에 악영향을 끼칠 수 있기 때문입니다.

따라서 메모리 관리에 있어서는 가비지 컬렉션에게 그 역할을 위임하는 것이 좋은 방향이라고 생각합니다.

728x90
반응형