본문 바로가기

공부/이득우의 언리얼 프로그래밍

[Study] Part1 - 언리얼 엔진의 메모리 관리 (12/15)

728x90
반응형

 

 

  • 정리
    • 언리얼 메모리 관리 시스템
      1. C++ 언어의 고질적인 문제인 포인터 문제의 이해
      2. 이를 해결하기 위한 가비지 콜렉션의 동작 원리의 이해와 설정 방법
      3. 다양한 상황에서 언리얼 오브젝트를 생성하고 메모리에 유지하는 방법의 이해
      4. 언리얼 오브젝트 포인터를 선언하는 코딩 규칙의 이해

 

 

  • C++ 언어 메모리 관리의 문제점
    • C++은 저수준으로 메모리 주소에 직접 접근하는 포인터를 사용해 오브젝트를 관리한다.
    • 그러다보니 직접 할당(new)해지(delete) 짝 맞추기를 해야 한다.
    • 잘못된 포인터 사용 예시
      • 메모리 누수
      • 허상 포인터
      • 와일드 포인터
    • 잘못된 포인터 값은 다양한 문제를 일으키며, 한번의 실수는 프로그램을 종료시킴
  • C++ 이후에 나온 언어 Java/C#은 이런 고질적인 문제를 해결하기 위해 포인터를 버리고 대신 가비지 컬렉션 시스템을 도입함
  • 가비지 컬렉션 시스템
    • 프로그램에서 더 이상 사용하지 않는 오브젝트를 자동으로 감지해 메모리를 회수하는 시스템
    • 동적으로 생성된 모든 오브젝트 정보를 모아둔 저장소를 사용해 사용되지 않는 메모리를 추적

가비지 컬렉션 시스템

 

 

  • 언리얼 엔진의 가비지 컬렉션 시스템
    • 지정된 주기마다 몰아서 없애도록 설정되어 있음. (GCCycle. 기본 값 60초)
    • 성능 향상을 위해 병렬 처리, 클러스터링과 같은 기능을 탑재함

 

 

Edit>Project Settings>Engine>Garbage Collection

 

 

 

  • 가비지 컬렉션을 위한 객체 저장소
    • 관리되는 모든 언리얼 오브젝트의 정보를 저장하는 전역 변수 : GUObjectArray
    • GUObjectArray 의 각 요소에는 플래그(Flag)가 설정되어 있음
    • 가비지 컬렉터가 참고하는 주요 플래그
      • Garbage 플래그
        • 다른 언리얼 오브젝트로부터의 참조가 없어 회수 예정인 오브젝트
      • RootSet 플래그
        • 다른 언리얼 오브젝트로부터 참조가 없어도 회수하지 않는 특별한 오브젝트

 

 

가비지 컬렉션을 위한 객체 저장소

 

 

 

  • 가비지 컬렉터의 메모리 회수
    • 가비지 컬렉터는 지정된 시간에 주기적으로 메모리를 회수한다.
    • Garbage 플래그로 설정된 오브젝트를 파악하고 메모리를 안전하게 회수함
    • Garbage 플래그는 수동으로 설정하는 것이 아닌, 시스템이 알아서 설정함

 

 

가비지 컬렉터의 메모리 회수

 

  • 한 번 생성된 언리얼 오브젝트는 바로 삭제가 불가능함
  • 루트셋 플래그의 설정
    • AddToRoot 함수를 호출해 루트셋 플래그를 설정하면 최초 탐색 목록으로 설정됨
    • 루트셋으로 설정된 언리얼 오브젝트는 메모리 회수로부터 보호받음
    • RemoveFromRoot 함수를 호출해 루트셋 플래그를 제거할 수 있음

 

 

루트셋 플래그의 설정

 

 

  • 콘텐츠 관련 오브젝트에 루트셋을 설정하는 방법은 권장되진 않음
  • 언리얼 오브젝트를 통한 포인터 문제의 해결
    • 메모리 누수 문제
      • 언리얼 오브젝트는 가비지 컬렉터를 통해 자동으로 해결
      • C++ 오브젝트는 직접 신경써야 함
    • 댕글링 포인터 문제
      • 언리얼 오브젝트는 이를 탐지하기 위한 함수를 제공함 ::IsValid()
      • C++ 오브젝트는 직접 신경써야 함
    • 와일드 포인터 문제
      • 언리얼 오브젝트에 UPROPERTY 속성을 지정하면 자동으로 nullptr로 초기화 해줌
      • C++ 오브젝트의 포인터는 직접 nullptr로 초기화 할 것
  • 프로그래머들이 많이 실수하는 부분이기 때문에 잘 파악해서 언리얼 오브젝트와 C++ 오브젝트의 메모리 관리를 각각 따로따로 신경을 써줘야 한다.
  • 회수되지 않는 언리얼 오브젝트
    • 언리얼 엔진 방식으로 참조를 설정한 언리얼 오브젝트
      • UPROPERTY로 참조된 언리얼 오브젝트 (대부분의 경우 이를 사용)
      • AddReferencedObject 함수를 통해 참조를 설정한 언리얼 오브젝트
    • 루트셋(RootSet)으로 지정된 언리얼 오브젝트

 

 

(오브젝트 선언의 기본 원칙) 오브젝트 포인터는 가급적 UPROPERTY로 선언하고, 메모리는 가비지컬렉터가 자동으로 관리하도록 위임한다.

 

 

 

  • 일반 클래스에서 언리얼 오브젝트를 관리하는 경우
    • UPROPERTY를 사용하지 못하는 일반 C++ 클래스가 언리얼 오브젝트를 관리해야 하는 경우
    • FGCObject클래스를 상속받은 후 AddReferencedObjects 함수를 구현한다.
    • 함수 구현 부에서 관리할 언리얼 오브젝트를 추가해 줌
  • 언리얼 오브젝트의 관리 원칙
    • 생성된 언리얼 오브젝트를 유지하기 위해 레퍼런스 참조 방법을 설계할 것
      • 언리얼 오브젝트 내의 언리얼 오브젝트
        • UPROPERTY사용
      • 일반 C++ 오브젝트 내의 언리얼 오브젝트
        • FGCObject의 상속 후 구현
    • 생성된 언리얼 오브젝트는 강제로 지우려 하지 말 것
      • 참조를 끊는다는 생각으로 설계할 것
      • 가비지 컬렉터에게 회수를 재촉할 수는 있음 (ForceGarbageCollection 함수)
      • 콘텐츠 제작에서 Destroy함수를 사용할 수 있으나, 결국 내부 동작은 동일함. (가비지 컬렉터에 위임)

 

 

	TObjectPtr<class UStudent> NonPropStudent;

	UPROPERTY()
	TObjectPtr<class UStudent> PropStudent;
  • MyGameInstance.h
void CheckUObjectIsValid(const UObject* InObject, const FString& InTag)
{
	if (InObject->IsValidLowLevel())
	{
		UE_LOG(LogTemp, Log, TEXT("[%s] 유효한 언리얼 오브젝트"), *InTag);
	}
	else
	{
		UE_LOG(LogTemp, Log, TEXT("[%s] 유효하지 않은 언리얼 오브젝트"), *InTag);
	}
}

void UMyGameInstance::Init()
{
	Super::Init();

	NonPropStudent = NewObject<UStudent>();
	PropStudent = NewObject<UStudent>();
}

void UMyGameInstance::Shutdown()
{
	Super::Shutdown();

	CheckUObjectIsNull(NonPropStudent, TEXT("NonPropStudent"));
	CheckUObjectIsValid(NonPropStudent, TEXT("NonPropStudent"));

	CheckUObjectIsNull(PropStudent, TEXT("PropStudent"));
	CheckUObjectIsValid(PropStudent, TEXT("PropStudent"));
}
  • MyGameInstance.cpp

 

  • 언리얼 오브젝트 선언에서 언리얼 오브젝트의 클래스 멤버 변수를 선언할때는 반드시 UPROPERTY()를 붙여줘야만 댕글링 포인터 문제에서 벗어날 수가 있다.

 

UPROPERTY()를 붙인 PropStudent는 유효하지만, 그렇지않은 NonPropStudent는 유효하지 않다고 나옴

 

  • 일반 C++클래스에서 언리얼 오브젝트를 관리할 때, FGCObject를 상속하고 해당 함수들을 구현해줘야 댕글링 포인터 문제가 발생하지 않는다.
    • AddReferencedObject()
    • GetReferencerName()

 

void FStudentManager::AddReferencedObjects(FReferenceCollector& Collector)
{
	if (SafeStudent->IsValidLowLevel())
	{
		Collector.AddReferencedObject(SafeStudent);
	}
}
  • StudentManager.cpp
class UNREALMEMORY_API FStudentManager : public FGCObject
{
public:
	FStudentManager(class UStudent* InStudent) : SafeStudent(InStudent) {}

	virtual void AddReferencedObjects(FReferenceCollector& Collector) override;

	virtual FString GetReferencerName() const override
	{
		return TEXT("FStudentManager");
	}


	const class UStudent* GetStudent() const { return SafeStudent; }

private:
	class UStudent* SafeStudent = nullptr;
};
  • StudentManager.h

 

 

 

 

 

 

 

해당 포스트는 인프런의 <이득우의 언리얼 프로그래밍 Part1 - 언리얼 C++의 이해>
강의를 수강하고 정리한 내용입니다.
 

이득우의 언리얼 프로그래밍 Part1 - 언리얼 C++의 이해 | 이득우 - 인프런

이득우 | 대기업 현업자들이 수강하는 언리얼 C++ 프로그래밍 전문 과정입니다. 언리얼 엔진 프로그래머라면 게임 개발전에 반드시 알아야 하는 언리얼 C++ 기초에 대해 알려드립니다., [사진] 언..............................

www.inflearn.com

 

 

 

 

728x90