-
[Unreal] 언리얼의 GC에 대해서 알아보자. 아주 간략히쾌락없는 책임 (공부)/Unreal 2022. 11. 7. 18:07반응형
개요
언리얼을 처음 접했을 때 스크립팅 언어로 C++을 사용하니 메모리 관리에 있어서 스마트 포인터를 적극 활용하는 등 여러 조치를 취해야 할 거라고 생각했습니다. 뭐 프로그래머가 직접 다 관리하는 줄 알았죠.
그런데 알고 보면 언리얼은 리플렉션 시스템을 통해서 GC를 관리한다는 사실에 생각보다 메모리 관리를 안 하게 됩니다. 다만 '정말 이대로 괜찮을까?' 하는 생각에 여러 자료들을 찾아본 바, 관련 내용들을 정리하기 위해서 이 글을 작성해 봅니다. 앞으로 제작하는 데 있어서 이런 부분에서의 최적화를 생각해서 나쁠 건 없으니깐요.
그냥 쓰던 UPROPERTY, 리플렉션 시스템으로 사용되었다
리플렉션(Reflection)은 프로그램이 실행시간에 자기 자신을 조사하는 기능입니다.
- Unreal Engine Blog (2014. Michael Noland)코드 작성을 하다 보면 UPROPERY 같은 매크로를 통해 속성을 지정하는 일이 많습니다. 이런 매크로들이 리플랙션에 해당 클래스, 변수, 함수를 공개하게 하는 역할을 하는 식으로 해줍니다.
일단 이런 리플렉션 시스템은 옵션입니다. 이게 시스템에 보이기를 바란다고 하면 특수 헤더를 include 해야 합니다.
#include "FileName....geterated.h"
이 헤더는 아마 C++클래스를 새로 만들 때 자동으로 들어가는 모습을 볼 수 있습니다. 이 헤더가 있으면 이제 UPROPERTY, UFUNCTION, USTRUCT 같은 것들을 사용할 수 있는 것이죠. 그리고 언리얼 4.6 이후에서는 UCLASS와 함께 GENERATED_BODY() 매크로가 따라오게 됩니다. 이런 매크로는 구조체, 클래스에 필수로 들어가야 하는데 추가적인 함수나 typedef를 주입해주기 때문이라고 합니다.
뭐 이런식으로 코드를 작성하게 되면 이후 UHT(Unreal Header Tool)가 프로젝트 컴파일 시점에 해당 정보를 수집하게 됩니다. 결국 자동적으로 들어온 헤더와 GENERATED_BODY()등을 통해 UPROPERTY 같은 것들을 작성하면 리플렉션 시스템이 해당 클래스, 변수 등을 노출할 수 있는 것이죠.
UBT(Unreal Build Tool) / UHT(Unreal Header Tool)
일단 자료를 찾아보니 UBT, UHT에 관한 이야기가 나오게 되었습니다. 언리얼은 이 둘을 이용해 리플렉션을 더 강화하게 되는데요. UBT는 전체 헤더 파일들 중 리플렉션 시스템에 들어오는 파일들을 기억하고 이후 이중 어떤 것이든 변경이 된 것이라면 UHT를 이용해서 리플렉션 데이터를 수집, 업데이트합니다.
이후 수집이 된 정보는 별개의 C++ 코드인 ~~~.generated.h, .cpp로 저장하게 됩니다.
GC
'음 이제 리플렉션 시스템이랑 대충 뒤에 어떻게 돌아가는지 안다'라는 생각이 들 때쯤 본격적으로 GC를 보게 됩니다. 일단 기본적으로 Mark and Sweep 방식과 비슷하게 GC가 동작하게 됩니다. 아마 Reference Count 방식으로는 상호 참조 문제가 해결되지 않는, shared_ptr과 비슷한 느낌이 되어버리니 이런 방식을 사용한 것 같습니다.
이제 정확히 어떤 동작을 통해서 이 GC가 동작하는지를 한번 알아보도록 하겠습니다.
일단 GC는 크게 4가지 동작을 하게 됩니다.
1. Mark Unreachable
2. Mark Reachable
3. Sweep
4. Shrink Hash Table1. Mark Unreachable
해당 과정은 병렬로 진행이 되며 모든 UObject들을 ParrallelFor를 통해 탐색하며 'Unreachable' 플래그를 세워둔다고 합니다. 번역 중에 여러 모르는 단어들이 있는데 일단 병렬 시행에 Unreachable 플래그를 세운다고 알아두면 될 것 같습니다.
이런 과정은 언리얼의 GarbageCollection.cpp > PerformReachabilityAnalysis(...) -> FRealtimeGC::MarkObjectsAsUnreachable 과정을 통해서 이루어진다고 합니다.
2. Mark Reachable
이 과정 역시 병렬로 진행이 되며 모든 루트 오브젝트들을 돌면서 UPROPERTY를 재귀적으로 따라간다고 합니다. 이 과정에서 이게 PendingKill, 지워지기로 예정된 오브젝트가 아니면 'Unreachable' 플래그를 지워준다고 합니다. 이 과정은 상당히 시간도 많이 걸리고 UObject들의 수가 많으면 더 오래 걸릴 수 있다고 합니다.
그러면 만약 PendingKill이 된 오브젝트면 어떻게 될까요. 일단 만나게 되면 더 이상 진행하지 않고(원문 : traverse - 참조를 더 따라가면서 검색하는 일을 하지 않는 걸로 해석됨) UPROPERTY를 null로 만들어 줍니다. 이런 작업으로 인해서 UObject들을 사용할 때 IsValid(..) 검사를 진행해야 하는 일이 생기게 되는 것입니다.
3. Sweep
이제 여기서 'Unreachable' 플래그가 붙은 오브젝트들을 전부 치워주게 됩니다. 그리고 유니티의 구린 GC와는 다르게 소스코드에 따라서 다중 스레드 지원을 해 줍니다.
이제 이런 가비지가 제거되면 이 단계에서 UObject의 해싱을 없애게 됩니다. (Unhash)
4. Shrink Hash Table
이제 위 Sweep 단계에서 모든 가비지 제거 후, 해시 테이블이 압축된 후에 불리게 되는 단계입니다. 이 해시 테이블은 엔진이 모든 UObject들을 순회하지 않고 자기 타입의 오브젝트를 찾을 수 있게 해주는 테이블입니다. 엔진의 로그에서 “Compacting FUObjectHashTables data took %6.2fms”. 같은 이야기가 보이면 이제 이 단계의 작업이 진행된 것입니다.
순회 과정은 알아낸 거 같은데 그러면 순회를 위한 오브젝트들을 어떻게?
일단 공식문서에 따르면 언리얼에서는 주기적으로 UObject를 관리하는 GC 스키마를 사용한다고 합니다. 엔진에서는 이 래퍼런스 그래프(Reference graph)를 만들어서 어떤 게 사용되는지 아닌지 알아낸다고 합니다.
그리고 위 그래프의 'Root' 에는 'Root set'이라 지정된 오브젝트의 set이 있다고 합니다. 어떤 오브젝트들도 이 set에 추가될 수 있으며 GC 발생 시 이 root set부터 알려진 UObject 레퍼런스 트리를 검색해 참조된 오브젝트를 전부 추적할 수 있다고 합니다.
GC와 함께 했을 때 주의점은 어떤 게 있을까?
이제 기본 동작은 알아본 거 같고(알고리즘, 리플랙션 이런 것들이 있다는 사실) 이제 이런 GC와 함께 할 때 코드 작성에서 주의할 점들이 어떤 것들이 있을지 한번 알아봤습니다.
UObject에서 포인터는 UPROPERTY를 추가하자
포인터는 클래스의 멤버 변수에 저장되어야 하며 위에 UPROPERTY를 추가해 GC에 드러나도록 해야 관리가 쉽습니다.
일반 클래스에서 메모리를 관리해야 하는 경우
이 경우에는 'TWeakObjectPtr' / 'FWeakObjectPtr'를 사용해서 관리하는 게 좋습니다. 더 나아가 모든 UObject*들이 UPROPERTY를 추가지 않을 수 있는데 이런 경우에는 TWeakObjectPtr을 사용하는 게 좋습니다.
이와 관련한 사용처를 더 알아보니 만약 오브젝트 리스트를 보여주는 UI 가 있다고 하면 이 경우에 UI에서는 TWeakObjectPtr을 사용해서 보여주는 게 맞습니다. 만일 그냥 포인터를 받아오게 되면 UI가 켜진 상태에서는 오브젝트들이 사라질 수 없으니깐요.
int32, float, bool 등의 타입들도 무언가를 해야 하나?
이 경우 아무것도 안 해도 됩니다. 여기서 사용하는 UPROPERTY는 대부분 블루프린트에 노출시키기 위해서 사용하게 되는 것이죠.
TArray는 컨테이너들 중 유일하게 UObject*를 안전하게 담을 수 있다
이게 무슨 이야기인지 계속 궁금해했습니다. 'TArray는 컨테이너 요소들이 GC 된다!' 라는데 이 말 뜻을 잘 이해를 못 했죠.
일단 좀 많이 돌아다닌 결과 TArray에 담긴 래퍼런스는 UPROPERTY를 붙여주지 않으면 리플렉션 시스템에 보이지 않는다! 는 이야기를 보게 되었습니다. 추가로 TArray는 UPROPERTY가 달려 있고 UObject 파생 포인터를 저장한 경우 요소를 가비지 컬렉팅 할 때 부가 혜택이 있다고 했습니다.
여기서 부가 혜택이 뭔지만 알면 이 소제목의 이유를 알 수 있을 거 같습니다... 하지만 이에 대한 정답을 구할 수 없어서 안타깝게 이유를 알아낼 수가 없었습니다. 추후 개발하면서 더 단서를 알게 되면 아마 글을 따로 작성하게 되지 않을까 하네요.
자잘한 이야기
Cast는 이런 리플렉션 시스템 덕분이다
자주 사용하게 되는 Cast 템플릿 함수의 경우 리플렉션 시스템 덕분에 작동할 수 있습니다. UObject는 항상 자신이 무엇인지 알고 런타임에 형식 결정 및 캐스팅이 될 수 있는 것입니다.
참고 자료
언리얼 프로퍼티 시스템 (리플렉션)
리플렉션(Reflection)은 프로그램이 실행시간에 자기 자신을 조사하는 기능입니다. 이는 엄청나게 유용한 데다 언리얼 엔진 테크놀로지의 근간을 이루는 것으로, 에디터의 디테일 패널, 시리얼라
www.unrealengine.com
언리얼 오브젝트 처리
UObject 시스템의 기능에 대한 개요입니다.
docs.unrealengine.com
- 언리얼 포럼에 있는 개발자의 GC 동작 설명
Garbage Collector Internals
Unreal Engine’s Garbage Collector is a standard Mark & Sweep collector. This post explains its stages and what it does under the hood. Summary These are the major stages of the Garbage Collector: Mark unreachable. Mark reachable. Sweep. Shrink Hash Table
forums.unrealengine.com
- GC와 함께 했을 때 코드에서의 주의점
Garbage Collection | Unreal Engine Community Wiki
Garbage Collection is a form of automatic memory management.
unrealcommunity.wiki
- Cast와 관련한 이야기
Unreal Object Handling
Overview of the features of the UObject system.
docs.unrealengine.com
반응형'쾌락없는 책임 (공부) > Unreal' 카테고리의 다른 글
[Unreal] 언리얼 블루프린트 vs C++, 왜 프로그래머는 C++을 써야 하는가? (0) 2023.03.10 [Unreal] 언리얼 인터페이스 이야기 - Unreal C++ Interface (0) 2022.11.28 [Unreal] 언리얼 Anim Montage가 재생되지 않을 때 - Unreal Anim Montage not playing (0) 2022.11.05 [Unreal] GarbageCollection.cpp 에러, Invalid object in GC 에러 (0) 2022.11.05 [Unreal] 언리얼 TObjectPtr에서 UFUNCTION 사용 불가 (0) 2022.11.04