👩🏻‍💻기초지식/Unreal

[Unreal] 가비지 컬렉션 (Garbage Collection)

공대 컴린이 2023. 8. 16. 12:12
728x90

정의

가비지 컬렉션이란 더이상 참조되지 않거나, 명시적으로 소멸을 예약시킨 UObject를 주기적으로 정리하는 기능이다.

가비지 컬렉션의 필요성

1. 메모리 누수

2. 댕글링 포인터(허상 포인터)의 발생

3. 미정의 동작

 

이러한 실수를 피할 수 있도록 필요 없어진 객체(가비지)는 알아서 삭제해주는 가비지 컬렉션이 필요하다.

 

언리얼의 가비지 컬렉션이 적용되는 오브젝트

1. UPROPERTY 레퍼런스를 유지하는 오브젝트

2. TArray, TMap 같은 언리얼 엔진 컨테이너 클래스로 저장된 오브젝트 안에 UObject 인스턴스를 저장하고 있을 때

 

1️⃣ 참조 카운터 방식의 가비지 컬렉션

C++의 shared_ptr이나, 언리얼의 TSharedPtr같은 스마트 포인터를 이용하여 단순한 가비지 컬렉션을 이용할 수 있다.

참조 카운트를 업데이트하며, 카운터가 0이 됨에 따라 delete를 호출하는 방식이다.

 

장점

이는, 개별 객체에 대해 '필요 없어짐'을 즉각적으로 감지할 수 있고,

프로그램이 할당한 메모리의 총량을 항상 최소한으로 유지할 수 있다는 장점이 있다.

 

단점

Row 포인터가 아니라 포인터 객체를 사용하는 방식이기 때문에, 복사/대입/역참조 같은 포인터의 기본 연산에 따라 약간씩의 성능 비용이 추가된다.

 

문제점

순환참조에 의한 메모리 누수가 발생할 수 있다.

따라서 소유권(ownership) 방향의 참조는 shared_ptr을 사용하고, 그 외에는 weak_ptr을 사용해야 한다.

 

2️⃣ Mark-Sweep 방식의 가비지 컬렉션

Mark - Sweep 방식은 참조 카운터 방식과 접근 자체가 많이 다르다.

Mark - Sweep 알고리즘의 한 사이클은 Mark 단계와 Sweep 단계로 나누어진다.

Mark 단계에서는 어떤 객체가 필요없어지더라도 당장 신경쓰지 않고 냅뒀다가, 메모리가 부족해지거나 정리할 필요가 있을 때 Sweep 단계에서 한번에 정리하는 방식으로 이루어진다.

 

> 언리얼 문서에서의 설명

더보기
언리얼 엔진에서는 레퍼런스 그래프를 만들어, 어느 오브젝트가 아직 사용중이고 어느것이 사용되지 않는지를 알아냅니다. 이러한 레퍼런스 그래프의 루트에는 "루트 세트"라고 지정된 오브젝트 세트가 있습니다. 어떤 오브젝트도 루트 세트에 추가할 수 있고, 가비지 컬렉션이 발생하면, 엔진은 루트 세트부터 시작해서 알려진 UObject 레퍼런스 트리를 검색하며 참조된 모든 오브젝트를 추적할 수 있습니다. 참조되지 않은 오브젝트, 즉 트리 검색에서 찾지 못한 것들은 더이상 필요치 않은 오브젝트라 가정하고 제거합니다.

 

포인터 연산 방식마다 약간의 성능 비용이 추가되는 참조 카운터 방식을 '할부'라고 한다면, Mark - Sweep 방식은 한번에 정리하므로 '일시불'로 볼 수 있다.

 

준비 단계

가비지 컬렉터가 메모리 관리자로써의 역할을 하기 때문에 내가 할당한 객체들의 주소가 어디어디인지를 전부 알고있다는것이 기본 전제이다.

Root Set은 전역변수/지역변수/함수의 파라미터 처럼 프로그램이 1차적으로 참조하는 것으로 보면 된다.

준비 단계에서는 모든 객체의 도달 가능 마킹을 해제한 상태에서 시작된다. 

도달 가능 마킹은 객체 내의 플래그 형태로 존재할수도 있고 별도의 자료구조로 존재할수도 있다.

 

Mark 단계

Mark 단계에서는 먼저, Root Set이 참조하는 객체들을 방문하고, 어떤 객체가 처음 방문되었을 때는 그 객체가 참조하는 객체들을 또 방문한다.

더이상 참조 가능한 객체가 없을 때까지 객체를 방문하여 '도달 가능' 마킹을 켜준다.

 

Sweep 단계

마지막까지 '도달 가능' 마킹이 켜지지 않은 객체들은 앞으로 프로그램에서 사용될 가능성이 없는 객체로 보고, 지워도 안전하다고 판단하여 지운다.

 

이때, 액터는 Destroy 함수, 컴포넌트는 DestroyComponent 함수를 사용하여 명시적으로 소멸 마킹을 처리할 수 있다.

 

객체를 가비지 컬렉션의 수집 대상에서 제외시키는 방법

아래 두 함수는 가비지 컬렉션의 사용을 원치 않는 Class의 경우, 수집 대상에서 제외시킬 수 있는 방법이다.

 

✏️ UObjectBaseUtility::AddToRoot

 

AddToRoot 함수를 사용하여 루트세트에 객체를 추가할 수 있다.

함수를 통해 루트세트에 객체를 추가하면, 가비지 컬렉션이 가비지를 수집하는 과정에서 추가한 객체와 모든 하위 항목들이 삭제되는것을 방지할 수 있다.

✏️ UObjectBaseUtility::SetFlags( EObjectFlag::RF_MarkAsRootSet )


SetFlags 함수를 통해서 플래그를 직접 설정하여 제외시킬수도 있다.

 

Mark-Sweep 가비지 컬렉터의 참조 포인터를 알아내는 방법

C++ 에서 객체의 참조는 포인터 변수를 의미한다.

가비지 컬렉터가 이러한 객체 내에서 포인터 변수의 위치를 알아내는 방법은, C++에서 객체의 레이아웃이 주어져야 하는지 아닌지에 따라 2가지 방법으로 나뉜다.

 

Conservative 컬렉터 혹은 Precise 컬렉터로 나뉘게 된다.

 

1️⃣ Conservative 가비지 컬렉터

위와 같은 Conservative 컬렉터는 유니티 엔진에서 Boehm-Demers-Weiser GC 라는 이름으로 사용되고 있다.

 

2️⃣ Precise 가비지 컬렉터

Precise 방식은 언리얼의 리플렉션 정보를 활용해 객체 내의 포인터 위치를 알아내는 방법으로 작동된다.

 

그러나, UObject의 클래스 안에서 모든 변수가 리플렉션 정보를 갖는것은 아니다.

 

언리얼 엔진이 기본으로 제공하는 자료구조가 아닌 자료구조를 사용하거나, 중첩된 배열을 사용하는 경우처럼 리플렉션 정보가 없는 경우, AddReferecedObjects 라는 함수를 구현하여 가비지 컬렉터에게 리플렉션 정보를 알려줄 수 있다.

 

즉, AddReferecedObjects 함수는 리플렉션 정보가 없는 포인터에 대해 가비지 컬렉터에게 알려주기 위한 통로로 사용된다.

 

TWeakPtr과 TWeakObjectPtr

리플렉션 된 Raw 포인터는 강한 참조이기 때문에, Raw 포인터가 가리키고 있는 동안은 대상 객체가 해제되지 않는다.

그러나, 해제되어도 무방한 객체라면 언리얼의 TWeakObjectPtr을 사용할 수 있다.

 

순환 참조를 방지하기 위해 사용된 TWeakPtr과 혼동되지 않도록 주의해야 한다.

TWeakObjectPtr은 순환참조를 없애기 위함이 아닌, 객체를 불필요하게 오랫동안 살려두지 않기 위함의 용도로 사용된다.

TWeakObjectPtr을 사용하면 IsValid를 통해 대상 객체가 해제되었는지를 확인할 수 있다.

 

TWeakObjectPtr의 개념 자체에 대해 좀 더 설명하자면, TWeakObjectPtr은 약포인터로, 가비지 컬렉션을 방지하지는 않지만 접근 전 IsValid 함수를 통해 유효성 검사가 가능하고, 거기서 가리키는 오브젝트가 소멸된 경우 Null 설정도 가능하다.

 

따라서, 언리얼 엔진에서는 raw 포인터로 저장된 오브젝트 레퍼런스가 자동으로 null 되거나, 가비지 컬렉션에 방지되지 않기 때문에 TWeakObjectPtr의 사용을 고려하고 있다.


참조 영상

https://www.youtube.com/watch?v=VpEe9DbcZIs&t=304s

https://docs.unrealengine.com/4.27/ko/ProgrammingAndScripting/ProgrammingWithCPP/UnrealArchitecture/Objects/Optimizations/

 

언리얼 오브젝트 처리

UObject 시스템의 기능에 대한 개요입니다.

docs.unrealengine.com

https://docs.unrealengine.com/4.26/en-US/API/Runtime/CoreUObject/UObject/UObjectBaseUtility/AddToRoot/

 

UObjectBaseUtility::AddToRoot

Add an object to the root set.

docs.unrealengine.com

https://docs.unrealengine.com/5.2/en-US/API/Runtime/CoreUObject/UObject/UObjectBaseUtility/SetFlags/

 

UObjectBaseUtility::SetFlags

Modifies object flags for a specific object

docs.unrealengine.com

 

728x90