GC 개념과 언리얼의 GC

Naezan·2023년 8월 20일
0

언리얼엔진

목록 보기
1/7

목차

  1. GC의 개념
  2. GC 알고리즘
  3. Mark and Sweep의 동작방식
  4. 언리얼 GC의 동작방식
  5. 언리얼 GC 디버깅

GC란?

동적으로 할당한 메모리(힙영역 메모리)더이상 쓰이지 않는 메모리를 자동으로 해제해주는 시스템입니다.

장점

  • 메모리 누수를 방지할 수 있다.
  • 해제된 메모리에 대한 접근을 막을 수 있다. → NullPointer참조 문제
  • 해제된 메모리를 또 해제하는 것을 막을 수 있다. → DoubleFree 문제

단점

  • GC작업은 프로그램에서 추가적으로 작업을 하는 것이기때문에 순수 오버헤드다.
  • 개발자 입장에서는 GC가 언제 동작하는지 정확하게 알지 못한다.

GC의 알고리즘 : 레퍼런스 카운팅, 마크 앤 스윕

레퍼런스 카운팅

메모리를 할당받은 객체들이 레퍼런스 카운트라는 숫자를 통해서 할당된 횟수를 가지고 있는 방식입니다.

레퍼런스 카운트가 0이 되면 알아서 할당해제 해주는 방식입니다.

단점

서로가 서로를 할당하는 순환참조 문제가 발생해 메모리 누수가 발생할 수 있다.

Mark and Sweep

루트에서 그래프 순회를 통해 해당 객체에 접근가능한지의 여부를 파악하고(Mark)
접근가능하지 않은 객체를 할당해제 해주는(Sweep) 방식입니다.

루트(GC Root)연결된 객체들Reachable, 연결되지 않은 객체들Unreachable이라고 부른다.

추가적으로 메모리 파편화를 막기위해 Compaction을 진행하기도 한다.

장점

  • 레퍼런스 카운팅 방식순환참조 문제를 해결할 수 있다.

단점

  • 의도적으로 GC를 실행시켜줘야한다.
  • 프로그램 실행과 GC의 실행을 병행해줘야한다.

Mark and Sweep의 동작방식(JVM)

메모리가 할당된 힙 영역을 Young GenerationOld Generation으로 나누고 YoundGeneration은 또다시 Eden, Survivor0, Survivor1로 나눕니다.

  1. 맨처음 Eden영역에 할당

  1. Eden영역이 가득차면 Minor GC 발생 → Rechable한 녀석은 Survivor영역으로 이동

  1. 살아남은 객체들은 Age가 증가

  1. 특정 Age(여기선 3)에 도달한 객체는 OldGeneration으로 이동

  1. OldNGeneration의 공간이 가득 차면 Major GC 발생

왜 이렇게 동작하게 만들었을까?

  • 대부분의 객체는 금방 unreachable하게 된다. → 대부분의 객체의 수명이 짧다.
  • 오래된 객체로의 참조는 거의 존재하지 않는다.

언리얼의 GC

언리얼엔진에서는 GC를 제공하지 않는 C/C++ 언어로 제작을 했기때문에 자체적인 GC시스템을 만들었습니다.

언리얼의 GC는 표준 GC모델을 사용하지만 멀티 스레드 기반 가비지 컬렉션(Multi-Threaded Garbage Collection)알고리즘을 지원해줍니다. 이때는 여러 개의 쓰레드를 사용하여 병렬적으로 GC를 수행합니다.

언리얼 GC트리 구조

언리얼에서는 RootSet객체를 지정해두고 RootSet부터 GC를 수행하게 됩니다.

이때 RootSet이란 영구적으로 활성상태인 객체를 뜻합니다.

언리얼 GC트리 순회방식

  1. RootSet에서 시작하여 리플렉션을 통해 RTTI를 동적으로 검사하여 객체간의 참조관계를 조사
  2. 찾은 객체가 루트집합에 포함된 경우 활성상태를 유지
  3. 루트집합에 도달하지 않은 경우 GC수집대상으로 등록

언리얼 GC의 동작방식

언리얼 엔진소스 GarbageCollection.cppCollectGarbageInternal(...)에서 수행되고 아래와 같은 순서를 따릅니다.

Mark unreachableMark reachableSweepShrink Hash Table(Compaction)

위 과정은 모두 CollectGarbageInternal(...)에서 실행되고 개념설명이 끝난 뒤, 모든 실행과정을 보여드리도록 하겠습니다.

Mark unreachable

PerformReachabilityAnalysis(..)FRealtimeGC::MarkObjectsAsUnreachable에서 수행되며 Unreachable한 경우 UnreachableFlag를 부여합니다.

Mark Reachable

PerformReachabilityAnalysis(..)FRealtimeGC::PerformReachabilityAnalysisOnObjectsInternal에서 수행되며 UObject갯수가 많을 수록 많은 시간이 소요됩니다.

대략 10만개 이상의 오브젝트가 존재할 경우 GC clusters를 사용하는 방법을 통해 시간을 줄일 수 있습니다.

또한 이 단계에서는 이득우 강사님의 12강 언리얼 메모리 강의에서 봤던 FGCObjectAddReferencedObjects함수를 통해 강제적으로 GC에 수집되지 않도록 해주는 과정이 포함됩니다.

Sweep

Unreachable한 객체를 파괴/제거하는 단계로 IncrementalPurgeGarbage()에서 수행됩니다. IncrementalPurgeGarbage()내부에서는 UnhashUnreachableObjects()함수를 통해 UObjectunhash하는 작업도 추가적으로 진행합니다.

Shrink Hash Table(Compaction)

모든 가비지가 제거된 후에 실행되는 해시 압축(Compact)단계로 ShrinkUObjectHashTables()FUObjectHashTables::ShrinkMaps()에서 수행됩니다.

여기서 해시테이블이란 모든 UObject객체반복적으로 검사하지 않고 유형에 따라 개체를 빠르게 가져올 수 있는 편리한 테이블입니다.

언리얼 GC 디버깅

언리얼에서는 GC와 관련된 로그를 볼 수 있는 기능을 제공해줍니다.

이 디버깅 기능으로 GC의 총 실행시간각 단계별로 걸린 시간GC디버깅 정보를 확인할 수 있습니다.

로그를 보기 위해서 먼저 콘솔에 log loggarbage verbose명령어를 입력해주면 됩니다.

그리고 GC주기를 본인이 원하는 시간으로 정하면 아래 그림처럼 로그를 확인할 수 있습니다.

그리고 콘솔 창에서 obj gc명령어를 통해서 강제로 GC를 발생시킬 수도 있습니다.

강제로 발생시키지 않으면 설정해둔 GC시간마다 발생하게 됩니다.

메모...

언리얼의 GC에 대한 동작방식은 실제로 직접 디버깅하면서 확인하면 더 명확하게 와닿을 수 있습니다.
소스코드에 Breakpoint를 찍고 GC의 흐름이 어떻게 동작하는지 직접 경험해보면서 배우셨으면 좋겠습니다.

참고자료

JVM (https://steady-coding.tistory.com/584)
.NET(https://prodotnetmemory.com/slides/UnderstadingGC/#14)
GC(https://mikelis.net/memory-management-garbage-collection-in-unreal-engine/)
엘리의 GC : https://youtu.be/Fe3TVCEJhzo
조엘의 GC : https://youtu.be/FMUpVA0Vvjw
언리얼 GC(https://forums.unrealengine.com/t/garbage-collector-internals/501800, https://forums.unrealengine.com/t/primer-debugging-garbage-collection-performance/661734#measuring-garbage-collection-times-2)

profile
게임 개발자

0개의 댓글

관련 채용 정보