본문 바로가기

UE4

[UE4] Garbage Collection overview

0. 서문

참조(레퍼런스)가 더 이상 존재하지 않는, 미사용되는 오브젝트들을 주기적으로 메모리에서 제거해 주는 일은 게임 엔진에서의 메모리 관리 측면에서 상당히 중요한 기능이다.
 
UE4는 이를 위해 Reflection 기능을 활용하는데, 효과적으로 언리얼 엔진에서 개발하기 위해선 이 두 가지 시스템이 어떻게 상호작용하는지 정확하게 이해할 필요가 있다.
 
참고로, 이 글은 Unreal Wiki의 Garbage Collection Overview 문서를 번역하면서 약간의 보충을 더하는 식으로 작성하였다.
(예를 들어, 언리얼 오브젝트 처리 같은 문서의 내용도 포함되는 등...)
또한 overview의 성격을 가지는 문서이므로, 너무 깊숙한 부분들까지는 다루지 않았다.
 

1. Reflection

UE4의 리플렉션에 대한 다음의 글을 먼저 이해한 다음에 나머지 부분을 읽어나갈 것을 추천한다.
리플렉션을 통해 엔진은 객체가 다른 객체에 의해 참조되고 있는지 여부를 결정할 수 있으므로, Garbage collection을 실행 가능할 수 있게 해 준다.
 

2. Garbage Collection

게임 엔진에서 가장 중요한 작업 중 하나는 메모리 관리이며, 언리얼 엔진4의 접근 방식은 Garbage collection을 수행하는 것이다.
이 접근법에서 엔진은 더 이상 필요없는 객체를 자동으로 삭제하며, 더 이상 필요없는 객체란 더 이상 다른 객체에 의해 참조되지 않음을 의미한다.
 
언리얼은 Garbage collection을 수행하기 위해 리플렉션 시스템을 사용하는데, 엔진이 객체와 속성들을 알고 있고 그러기에 더 이상 사용되지 않아 삭제되어도 좋은 객체들을 구분할 수 있기 때문이다.
 
더 구체적으로 얘기하면, 엔진에서는 Reference Graph를 만들어 오브젝트들의 사용 여부를 구분한다.
이 그래프 루트에는 "Root Set" 이라 지정된 오브젝트 세트가 존재하며, "Root Set"에 포함된 녀석들은 Garbage Collection 대상에서 제외된다. 어떤 언리얼 오브젝트도 UObjectBaseUtility::AddToRoot 함수를 통해 "Root Set"에 추가시킬 수 있다.
(Ex. UObjectXXX->AddToRoot())
 
가비지 콜렉션이 실행되면, 엔진은 "Root Set"부터 시작해 알려진 UObject 레퍼런스 트리를 검색하여 참조된 오브젝트를 전부 추적한다. 참조되지 않는 오브젝트, 즉 트리 검색에서 찾지 못한 것들은 더 이상 필요하지 않은 오브젝트라 판단하고 제거할 수 있는 것이다.
 
엔진에서의 자동 메모리 관리를 통해 엄청난 (프로그래머의) 정신적 작업량을 줄일 수 있지만, "규칙"을 지켜야만 제대로 작동하므로, 적어도 상위 수준의 작동 방식에 대한 이해는 필요하다.
 
다음의 "규칙"들에 대해 정확하게 숙지하도록 하자.
 

1) "생명 주기를 함께 할" 멤버 변수는 UPROPERTY로 선언하자

엔진은 "naked" 멤버 변수의 메모리에 대해 파악하지 못한다.
따라서, 가리키고 있는 특정 메모리 영역이 나도 모르게 지워질 수 있다.
 
int, bool 같은 primitive 타입들은 "naked" 멤버 변수여도 문제가 없다.
물론 이 경우 C++ 내부에서만 유효하다. 즉, 저장되거나 네트워크 상에서 복제되거나 에디터에 나타나진 않는다.
 
이러한 경우 외 언리얼 오브젝트(UObject)를 멤버 변수로 가질 경우, 그리고 이 멤버 변수가 소유자의 생명 주기와 함께 할 녀석이라면 UPROPERTY 매크로를 붙여주어야 한다. 그렇지 않으면 외부에서 지워져 버릴 수 있다.
 
그렇지 않은 잠시 소유할 UObject나 일반 클래스(F로 시작하는...)의 객체들은 별도로 관리해 줄 필요가 있다.
 

2) 멤버가 가리키는 포인터는 UObject 또는 그 자식들로 한정하자

엔진이 인식하고 관리하지 못하는 메모리 영역을 가리키는 포인터는 늘 위험하다.
실질적으로 참조(가리키는 것)하고 있지만, 엔진에서 알 수가 없기에 Garbage collection 단계에서 지워질 지 모르기 때문이다.

 

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

TArray에 대해 더 자세히 알고 싶다면, 아래 링크된 문서를 살펴보면 된다.
다시 한번 강조하지만, garbage collection은 reflection data에 의존한다.
 

3. UStructs vs UObjects

구조체는 "value" 타입으로 사용하기 위한 것이다.
객체와 액터내에서 재사용되는 작은 데이터 크기 가장 적합하게 사용된다.
예를 들어, FVector, FRotator, FQuat 등이 있다.
 
구조체는 garbage collection의 수집 대상이 아니며, 그렇기에 구조체들은 UObject 내에 위치해야 한다.
그래야 UObject가 가지비-수집될 때 함께 처리될 수 있는 것이다.
 
UStruct의 장점 중 하나는 그것이 매우 작다는 것이다.
UObject는 데이터 외에도 book-keeping 데이터를 가져야 하지만, UStruct (기술적으로 UScriptStruct)는 사용자가 입력한 데이터만큼의 크기만 가진다.
그러나, UStruct에는 다른 객체의 멤버 구조체를 직접 가리키는 것이 안전하지 않은 제약이 존재한다.
 
UObject는 가비지-수집 되기에, UStruct에 비해 무거운 반면 일반적으로 안전하게 포인팅할 수 있다.
 

4. Garbage Collect 요청

UObject와 AActor의 파생 객체들에 대해 가비지 수집을 명시적으로 요청할 수 있다.
주의해야 할 점은, 이 요청들이 즉시 GC를 부르는 게 아니라, GC 수행시 대상으로 등록하라는 요청이라는 것이다.
 

1) UObject

UObject::ConditionalBeginDestroy()
 

2) AActor

AActor::DestroyActor()
 
특정 객체가 가비지 수집되었는지 알아내려면, 타이머를 하나 돌려서 IsValidLowLevel() == true인지 체크해 보면 된다.
참고로, 가비지 수집 대상인 객체를 사용하기 전에는 IsValidLowLevel() 함수로 체크해 보는 습관을 들이는 것을 추천한다.
 

5. TWeakObjectPtr

언리얼 오브젝트의 메모리 관리(즉, GC)는 shared_ptr과 동일한 방식(참조 카운팅 방식)으로 동작하기 때문에, 순환 참조 문제에서 자유롭지 못하다. 그래서 언리얼 C++은 약참조를 위한 TWeakObjectPtr 이라는 별도의 템플릿 라이브러리를 제공하고 있다.
 
특정 언리얼 오브젝트를 참조할 때, 소유권이 꼭 필요치 않은 상황이라면 TWeakObjectPtr을 통해 약참조를 거는 습관을 들이는 것을 추천한다.
 
예를 들어, 특정 UI에서 언리얼 오브젝트의 목록을 보여주고 싶을 때 TWeakObjectPtr을 사용해 언리얼 오브젝트를 약참조(Weak Referencing)하는 것이 바람직하다는 이야기이다. 일반적인(공유) 참조를 걸게 되면 UI가 띄워져 있는 동안에는 UI에서 보여지는 모든 언리얼 오브젝트의 레퍼런스 카운팅이 올라가게 되고, 다른 곳에서 언리얼 오브젝트를 삭제한다 해도 GC 시스템에서 회수가 일어나지 않게 된다. 물론 UI를 닫으면 그 다음 GC에서 회수가 되긴 하겠지만 말이다.
 

6. 강제 Garbage Collect 수행

 
World::ForceGarbageCollection(bool bFullPurge) 함수를 통해 강제 GC를 수행할 수 있다.
 
GC는 기본적으로 시스템이 알아서 수행하기 때문에, 개발자의 희망 시점과는 무관하게 실행된다.
게임이 진행되는 도중보다 가급적 로딩 타임에 가비지가 수집되면 좋기에 로딩 시점에 한번씩 강제로 실행해주는 것도 나쁘지 않을 수 있다. 
 
"나쁘지 않을 수 있다"라고 표현한 것은 이것이 늘 좋은 결과만을 가져오진 않기 때문이다.
프로젝트마다 적지 않은 시행 착오를 거쳐서 균형 잡힌 강제 GC 수행 패턴을 찾아야 할 것이다.
 

7. Project setting - Garbage collection

[프로젝트 세팅]-[엔진]-[Garbage Collection]에 들어가면, 여러 가지 항목들에 대한 설정을 변경할 수 있다.
항목별 자세한 내용은 다음의 문서를 참고하기 바란다.