언리얼 엔진 5/공부

[언리얼 엔진 5] 가비지 컬렉션(+유니티와의 차이), 스마트 포인터

ciel45 2024. 9. 29. 23:07

C++에는 가비지 컬렉션 기능이 없기 때문에, 언리얼 엔진에서는 이를 자체적으로 만들어 사용한다.

 

가비지 컬렉션은 리플렉션 시스템에 의존하는 엔진 내 기능 중 하나이다.

리플렉션 시스템에 대해 자세한 내용은 https://ciel45.tistory.com/127

 

[언리얼 엔진 5] 리플렉션 시스템

리플렉션이란, 프로그램이 실행 중 자기자신을 조사하는 기능을 의미한다. 에디터의 디테일 패널, 직렬화, 가비지 컬렉션, 네트워크 리플리케이션, 블루프린트/C++ 간 커뮤니케이션 등 엔진 내

ciel45.tistory.com

 


가비지 컬렉션이란?

  • 우리가 C++로 동적 할당을 사용할 때 new, delete를 사용하는데, 실수로 사용하지 않는 객체에 delete를 안하면 메모리 누수가 발생한다.
  • 또한 실수로 사용 중인 객체를 delete해버리면 해당 객체를 가리키던 포인터는 null을 가리키게 되고, 이를 dangling 포인터라 한다.
  • 그래서 원활한 메모리 관리를 위해 동적 할당한 메모리를 자동으로 해제해주는 것이 가비지 컬렉션이라고 할 수 있다.

 

언리얼 엔진의 가비지 컬렉션은 Mark and Sweep방식을 사용한다. (유니티와 마찬가지)


Mark and Sweep이란?

  • 객체가 필요 없어지더라도 일단 냅뒀다가, 메모리가 부족할 시 알고리즘을 돌려서 한번에 정리하는 방식이다.
  • 한 사이클이 Mark와 Sweep 두 단계로 이루어져 있다.
    • Mark 단계
      • 모든 객체의 마킹을 지워놓는다.
      • 어플리케이션의 루트로부터 순회하면서 도달하는 객체들을 마킹한다.
        • 어떤 객체가 루트로부터 찾아갈 수 있다면 마킹이 되는 것이다.
        • 트리처럼 탐색하면서 마킹을 하는 것과 같다.
    • Sweep 단계
      • 다 돌았는데 마킹이 되어있지 않은 객체들은 할당을 해제한다.

언리얼과 유니티의 가비지 컬렉션의 차이?

  • 가비지 컬렉터가 객체 내에서 포인터 변수들의 위치를 알아내는 방식에서 차이가 존재한다.
  • 해당 방식에 따라 Mark and Sweep 가비지 컬렉션은 다시 Conservative / Precise로 분류된다.
    • Conservative
      • 유니티가 사용하는 방식이다. 
      • 객체를 크기만 주어진 메모리 블록으로 취급한다. 즉 객체의 구체적인 생김새를 모르는 채로 동작한다.
      • 이 메모리 블록 내 모든 비트 패턴을 스캔해서, 내가 할당해준 객체로의 주소가 등장하면 그걸 포인터로 간주한다.
      • Conservative GC 중 대표적인 것이 유니티가 사용하는 Boehm-Demers-Weiser GC이다.
        • Stop the world 방식이다. (GC가 돌아가는 동안에는 모든 스레드 정지)
        • 세대, 압축 기능을 지원하지 않는다. (유니티 GC와 C# GC와의 차이점)
          • 세대 : 오래 살아남은 객체가 앞으로도 오래 살 것이라는 가정 하에 그룹을 나눠 관리하는 기능
          • 압축 : 객체들의 할당 해제 후엔 메모리 곳곳에 빈 공간이 생기는데, 이걸 쭉 밀어서 압축해주는 기능
            • 따라서 유니티 프로그래밍에서는 오브젝트 풀과 같은 메모리 단편화를 막기 위한 테크닉이 중요해진다.
    • Precise
      • 언리얼 엔진이 사용하는 방식이다.
      • 객체 내 포인터들의 위치를 정확하게 파악한 채로 동작한다.
      • 언리얼 엔진에서는 리플렉션 정보를 활용해 객체 내 포인터들의 위치를 파악한다.
        • UProperty 객체가 GC에게 모든 UObject 포인터들의 위치를 알려준다.

또한 언리얼 엔진은 메모리 관리를 위해 스마트 포인터 역시 지원한다.

 

스마트 포인터

  • C++ 에서는 std::shared_ptr, 언리얼에서는 TSharedPtr라는 이름으로 쓰인다.
  • 포인터 대신 포인터 객체를 사용한다.
    • 별도로 할당된 메모리 블록을 가리키면서, 거기에 대상 객체로의 참조 카운터를 계속 업데이트한다.
    • 카운터가 0이 되면 객체에 대해 delete를 호출한다.
  • 장점:
    • 어떤 객체가 필요 없어졌다는 사실을 즉각적으로 감지할 수 있다.
  • 단점:
    • 포인터로 객체를 사용하기 때문에 복사/대입/역참조 등의 포인터 연산이 살짝씩 더 무거워진다.
    • 순환 참조로 인한 메모리 누수 위험
      • 객체들끼리 서로 가리키면 참조 카운트가 영원히 0이 되지 않아 메모리 누수가 발생한다.
      • 이를 해결하기 위해 소유권이 있다고 여겨지는 쪽은 TSharedPtr(강한 참조)를 사용하고, 그렇지 않은 쪽은 TWeakPtr(약한 참조)를 사용하게 함으로써 사이클을 끊을 수 있다.
        • 약한 참조 횟수는 메모리 해제에 관여하지 않기 때문.

 

 

참조:

https://dev.epicgames.com/documentation/ko-kr/unreal-engine/unreal-object-handling-in-unreal-engine

https://www.youtube.com/watch?v=VpEe9DbcZIs&ab_channel=NHNCloud

(좋은 강의 감사합니다)