언리얼 엔진5 Basic - 메모리 관리

타입·2024년 3월 26일
0

언리얼 강의

목록 보기
4/47

언리얼 엔진의 메모리 관리

언리얼 엔진의 자동 메모리 관리

  • C++ 언어 메모리 관리의 문제점
    C++은 저수준으로 메모리 주소에 직접 접근하는 포인터를 사용해 오브젝트 관리
    프로그래머가 직접 할당(new)과 해지(delete) 짝 맞추기 필요
    잘못된 포인터 값은 다양한 문제를 일으키며 한 번의 실수로 프로그램이 종료
    • 잘못된 포인터 사용 예시
      • 메모리 누수(Leak)
        new를 했는데 delete 짝을 맞추지 못해 힙에 메모리가 그대로 남아있음
      • 허상(Dangling) 포인터
        다른 곳에서 이미 해제해 무효화된 오브젝트의 주소를 가리키는 포인터
      • 와일드(Wild) 포인터
        값이 초기화되지 않아 (운영체제가 보호하는) 엉뚱한 주소를 가리키는 포인터
    • C++ 이후에 나온 언어 Java/C#은 이런 고질적인 문제 해결을 위해 포인터를 버리고 대신 가비지 컬렉션 시스템을 도입
  • 가비지 컬렉션 시스템
    프로그램에서 더 이상 사용하지 않는 오브젝트를 자동으로 감지해 메모리를 회수하는 시스템
    동적으로 생성된 모든 오브젝트 정보를 모아둔 저장소를 사용해 사용되지 않는 메모리를 추적
    • 마크-스윕(Mark-Sweep) 방식의 가비지 컬렉션
    1. 저장소에서 최초 검색을 시작하는 루트 오브젝트를 표기한다.
    2. 루트 오브젝트가 참조하는 객체를 찾아 마크한다. (Mark)
    3. 마크된 객체로부터 다시 참조하는 객체를 찾아 마크하고 이를 계속 반복한다.
    4. 저장소에는 마크된 객체와 마크되지 않은 객체의 두 그룹으로 나뉜다.
    5. 가비지 컬렉터가 저장소에서 마크되지 않은 객체(가비지)들의 메모리를 회수한다. (Sweep)
  • 언리얼 엔진의 가비지 컬렉션 시스템
    마크-스윕 방식의 가비지 컬렉션 시스템을 자체적으로 구축
    지정된 주기마다 몰아서 없애도록 설정되어 있음 (GCCycle - 기본 값은 60초로 프로젝트 세팅에서 조절)
    성능 향상을 위해 병렬 처리와 클러스터링과 같은 기능을 탑재
  • 가비지 컬렉션을 위한 객체 저장소
    관리되는 모든 언리얼 오브젝트의 정보를 저장하는 전역 변수 GUObjectArray
    GUObjectArray의 각 요소에는 플래그가 설정되어 있음
    • 가비지 컬렉터가 참고하는 주요 플래그
      Garbage 플래그: 다른 언리얼 오브젝트로부터의 참조가 없어 회수 예정인 오브젝트
      RootSet 플래그: 다른 언리얼 오브젝트로부터 참조가 없어도 회수하지 않는 특별한 오브젝트
    • 가비지 컬렉터는 GUObject에 있는 플래그를 확인해 빠르게 회수해야 할 오브젝트를 파악하고 메모리에서 제거함

GUObject에 전역을 의미하는 G가 붙음
언리얼 엔진이 활성화되면 누구나 GUObject에 접근할 수 있음

  • 가비지 컬렉터의 메모리 회수
    가비지 컬렉터는 지정된 시간에 따라 주기적으로 메모리를 회수
    Garbage 플래그로 설정된 오브젝트를 파악하고 메모리를 안전하게 회수
    Garbage 플래그는 수동으로 설정하는 것이 아닌, 시스템이 알아서 설정함
    한 번 생성된 언리얼 오브젝트는 바로 삭제가 불가능함 (레퍼런스 정보를 없애고 언리얼 GC가 회수하도록 설정)

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

    콘텐츠 관련 오브젝트에 루트셋을 설정하는 방법은 권장되진 않음

  • 언리얼 오브젝트를 통한 포인터 문제의 해결
    • 메모리 누수 문제
      언리얼 오브젝트는 가비지 컬렉터를 통해 자동으로 해결
      C++ 오브젝트는 직접 신경써야 함 (스마트 포인터 라이브러리 활용)
    • 댕글링 포인터 문제
      언리얼 오브젝트는 이를 탐지하기 위한 함수를 제공 ::IsValid()
      C++ 오브젝트는 직접 신경써야 함 (스마트 포인터 라이브러리 활용)
    • 와일드 포인터 문제
      언리얼 오브젝트에 UPROPERTY 속성을 지정하면 자동으로 nullptr로 초기화 해줌
      C++ 오브젝트 포인터는 직접 nullptr로 초기화 필요 (스마트 포인터 라이브러리 활용)
  • 회수되지 않는 언리얼 오브젝트
    • 언리얼 엔진 방식으로 참조를 설정한 언리얼 오브젝트
      UPROPERTY로 참조된 언리얼 오브젝트 (대부분의 경우 이를 사용)
      AddReferencedObject 함수를 통해 참조를 설정한 언리얼 오브젝트 (UPROPERTY 사용 불가한 경우)
    • 루트셋으로 지정된 언리얼 오브젝트
      이 방식은 해당 오브젝트가 중요하다는 걸 의미하여 많이 사용되진 않음

오브젝트 선언의 기본 원칙

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

  • UPROPERTY를 사용하지 못하는 일반 C++ 클래스에서 언리얼 오브젝트를 관리하는 경우
    FGCObject 클래스를 상속받은 후 AddReferencedObjects 함수를 구현
    함수 구현 부에서 관리할 언리얼 오브젝트를 추가
class UNREALMEMORY_API FStudentManager : public FGCObject
{
	...
	virtual void AddReferencedObjects(FReferenceCollector& Collector) override;
    virtual FString GetReferencerName() const override
    {
    	return TEXT("FStudentManager");
    }
private:
	class UStudent* SafeStudent = nullptr;
};
  • 언리얼 오브젝트의 관리 원칙
    • 생성된 언리얼 오브젝트를 유지하기 위해 레퍼런스 참조 방법을 설계할 것
      언리얼 오브젝트 내의 언리얼 오브젝트: UPROPERTY 사용
      일반 C++ 오브젝트 내의 언리얼 오브젝트: FGCObject의 상속 후 구현
    • 생성된 언리얼 오브젝트는 강제로 지우려 하지 말 것
      참조를 끊는다는 생각으로 설계
      가비지 컬렉터에게 회수를 재촉할 수 있음 (ForceGarbageCollection 함수)
      콘텐츠 제작에서 Destroy 함수를 사용할 수 있으나 바로 제거하는 것이 아닌 플래그를 설정하여 GC가 자동으로 회수 (가비지 컬렉터에 위임)

가비지 컬렉션 테스트

  • 언리얼 오브젝트
// MyGameInstance.h
UCLASS()
class UNREALMEMORY_API UMyGameInstance : public UGameInstance
{
	GENERATED_BODY()
    
public:
	virtual void Init() override; // 어플리케이션이 초기화될 때 호출
    virtual void Shutdown() override; // 어플리케이션이 종료될 때 호출
    
private:
	TObjectPtr<class UStudent> NonPropStudent;
	UPROPERTY()
	TObjectPtr<class UStudent> PropStudent;
    
    TArray<TObjectPtr<class UStudent>> NonPropStudents;
    UPROPERTY()
    TArray<TObjectPtr<class UStudent>> PropStudents;
};


// MyGameInstance.cpp
void CheckUObjectIsValid(const UObject* InObject, const FString& InTag)
{
	// ::IsValid() 대신 보다 정교하게 체크해줄 수 있는 함수 사용
    if (InObject->IsValidLowLevel())
    	UE_LOG(LogTemp, Log, Text("[%s] 유효한 언리얼 오브젝트"), *InTag);
    else
    	UE_LOG(LogTemp, Log, Text("[%s] 유효하지 않은 언리얼 오브젝트"), *InTag);
}
void CheckUObjectIsNull(const UObject* InObject, const FString& InTag)
{
    if (InObject == nullptr)
    	UE_LOG(LogTemp, Log, Text("[%s] nullptr 언리얼 오브젝트"), *InTag);
    else
    	UE_LOG(LogTemp, Log, Text("[%s] nullptr 아닌 언리얼 오브젝트"), *InTag);
}

void UMyGameInstance::Init()
{
	Super::Init();
    
    NonPropStudent = NewObject<UStudent>();
    PropStudent = NewObject<UStudent>();
    
    NonPropStudents.Add(NewObject<UStudent>());
    PropStudents.Add(NewObject<UStudent>());
}
void UMyGameInstance::Shutdown()
{
	Super::Shutdown();
    
    CheckUObjectIsNull(NonPropStudent, TEXT("NonPropStudent")); // nullptr 아님 (댕글링 포인터)
    CheckUObjectIsValid(NonPropStudent, TEXT("NonPropStudent")); // 유효하지 않음
    
    CheckUObjectIsNull(PropStudent, TEXT("PropStudent")); // nullptr 아님
    CheckUObjectIsValid(PropStudent, TEXT("PropStudent")); // 유효함
    
    
    CheckUObjectIsNull(NonPropStudents[0], TEXT("NonPropStudents")); // nullptr 아님 (댕글링 포인터)
    CheckUObjectIsValid(NonPropStudents[0], TEXT("NonPropStudents")); // 유효하지 않음
    
    CheckUObjectIsNull(PropStudents[0], TEXT("PropStudents")); // nullptr 아님
    CheckUObjectIsValid(PropStudents[0], TEXT("PropStudents")); // 유효함
}
  • C++ 클래스 내의 언리얼 오브젝트
    매니저는 C++ 클래스라 UObject를 관리할 수 있는 능력이 없음..
    UPROPERTY를 쓸 수 없어 GC 발동되면 언리얼 오브젝트가 회수됨
// StudentManager.h
class UNREALMEMORY_API FStudentManager
{
public:
	FStudentManager(class UStudent* InStudent) : SafeStudent(InStudent) {}
    const class UStudent* GetStudent() const { return SafeStudent; }
private:
	class UStudent* SafeStudent = nullptr;
};


// MyGameInstance.h
UCLASS()
class UNREALMEMORY_API UMyGameInstance : public UGameInstance
{
	...
    class FStudentManager* StudentManager = nullptr;
};


// MyGameInstance.cpp
void UMyGameInstance::Init()
{
	Super::Init();
    
    StudentManager = new FStudentManager(NewObject<UStudent>());
}
void UMyGameInstance::Shutdown()
{
	Super::Shutdown();
    
    const UStudent* StudentInManager = StudentManager->GetStudent();
    
    delete StudentManager;
    StudentManager = nullptr;
    
    CheckUObjectIsNull(StudentInManager, TEXT("StudentInManager")); // nullptr 아님 (댕글링 포인터)
    CheckUObjectIsValid(StudentInManager, TEXT("StudentInManager")); // 유효하지 않음
}
  • 해결 방법
// StudentManager.h
class UNREALMEMORY_API FStudentManager : public FGCObject
{
	...
    virtual void AddReferencedObjects(FReferenceCollector& Collector) override
    {
    	if (SafeStudent->IsValidLowLevel())
        	Collector.AddReferencedObject(SafeStudent);
	}
    virtual FString GetReferencerName() const override
    {
    	return TEXT("FStudentManager");
    }
    ...
};


// MyGameInstance.cpp
void UMyGameInstance::Shutdown()
{
	...
    
    CheckUObjectIsNull(StudentInManager, TEXT("StudentInManager")); // nullptr 아님
    CheckUObjectIsValid(StudentInManager, TEXT("StudentInManager")); // 유효함
}
profile
주니어 언리얼 프로그래머

0개의 댓글