SungJin Kang

SungJin Kang

hour30000@gmail.com

© 2024

Dark Mode

게임 엔진에서 메모리 누수를 줄이기 위한 전략 ( 게임 엔진의 오브젝트 관리 )

new 할당한 오브젝트의 포인터를 잃어버리게 되면 영원히 해재할 수 없게된다.
각 모듈, 클래스에서 new 할당한 오브젝트를 직접 관리하게 되면 dangling 포인터를 만들 수 있고 오브젝트를 파괴하는 것을 까먹을 수도 있기 때문에 게임 엔진의 메모리 관리 측면에서 매우 좋지 않다.

이를 막기 위해서는 게임 엔진 내에서 사용하는 여러 오브젝트들을 관리하는 어떤 매니저 주체가 필요하다.
오브젝트가 생성되면 생성자 단계에서 해당 오브젝터의 포인터를 별도로 한 곳에 모아서 관리하면 적어도 생성한 오브젝트를 놓치는 일은 발생하지 않는다.
오브젝트가 파괴될 때는 매니저에서도 삭제하면 된다.

이건 사실 인턴을 하면서 실무자분들이 실무에서 이렇게 오브젝트를 관리한다고 알려주신 방법이다. ( 내가 있던 팀이 MMORPG 팀이었기 때문에 이러한 오브젝트 관리가 더 중요했을 것 같다. )
사실 이전에는 그냥 내가 실수하지 않으면 되지하고 안일하게 생각했는데 이 방법을 알고 나니 훨씬 부담을 덜었다.
그리고 해쉬테이블로 오브젝트 포인터들을 관리하면 오브젝트ID를 가지고 원하는 오브젝트를 빠르게 찾을 수도 있다.
또한 씬이 바뀌거나 오브젝트들을 한꺼번에 삭제해버리고 싶으면 모아두었던 오브젝트 포인터들을 순회하면서 파괴해주면 된다.
매우 간편하다.

실제로 언리얼 엔진을 보면 엔진내의 거의 모든 클래스는 최상단에 UObject 클래스를 조상 클래스로 두고 있다.

다만 이 방법도 현재 문제가 있어 보인다.
일단 new 할당을 하지 않은 오브젝트를 delete로 파괴해버릴 가능성이 존재한다.

class A
{
    B b;
};
A* a = new A();

위의 경우 오브젝트 a와 b 모두 매니저에서 관리되고 있는데 만약 a를 파괴하기 전 b를 파괴하면 오브젝트 b는 new로 할당된 오브젝트가 아니기 때문에 delete로 파괴하려고 하면 exception이 발생한다.
내 생각에는 이를 막기 위해서는 new 할당한 오브젝트와 그렇지 않은 오브젝트를 구분해주어야한다.
가장 쉬운 방법은 new를 사용하는 대신 new 할당을 wrapping한 함수를 따로 만들어주면 될 것 같다.
Variadic Template을 사용하면 생성자의 매개 변수 전달도 쉽게 구현할 수 있다.
이 new 할당을 wrapping한 함수를 사용할 때는 오브젝트 생성 후 어떤 Flag를 셋팅해주어서 new 할당한 오브젝트와 그렇지 않은 오브젝트를 구분해주면 될 것 같다.

실제 언리얼에서도 UObject는 new 할당을 금지하고 new를 wrapping한 NewObject라는 함수를 사용한다. ( 사실 위의 이유 때문인지는 모른다. )

아래 사진은 게임이 완전히 로드 된 후 생성되어 있는 DObject들이다. 4000개 가량의 DObject가 프로그램에 존재한다.
20211005004506

아래 사진은 이 글에서 소개한 전략으로 DObject들을 관리하고 일괄적으로 파괴한 후의 남은 DObject의 개수이다. 남은 1개는 전역 변수로 프로그램 종료시 해제될 것이다.
20211005011520

소스코드


코드를 직접 보지는 못했지만 언리얼 엔진의 IsValid보다 강력한 전달된 오브젝트 주소가 dummy 값이 아닌지, null 값인지, 가비지 컬렉터에 의해 회수가 대기 중인지 아닌지를 판단하기 위해서는 이 글에서와 같이 UObject들의 주소를 해쉬테이블로 관리해서 주소가 전달되면 해당 주소를 키 값으로 탐색을 수행해 valid한지를 판단할 것 같다.

인턴 기간 동안 언리얼 엔진을 사용하면서 느낀 것은 안전한 프로그래밍을 위해서 IsValid 함수를 정말 많은 곳에서 사용했었다. null 참조 문제는 치명적이니 안전하게 코드를 짜는 과정에서 IsValid 함수를 많이 사용했고 필자가 생각하기에는 이 IsValid 함수는 매 프레임 오브젝트 마다 몇 십번도 호출될 수 있다고 생각하였다.

그래서 이 IsValid 함수의 최적화가 중요하다고 생각한다.

현재는 unordered_map으로 오브젝트를 관리하는데 이렇게 하면 해시테이블의 버킷 속 아이템들이 링크드 리스트 형태로 구성되어서 캐시 측면에서 좋지 않을 것 같다.

나중에 std::vector로 크게 할당해두고 std::vector를 가지고 HashMap을 구현해서 사용해야할 것 같다. ( 메모리를 더 사용하는 대신 더 빠른 방법 )
혹은 unordered_map의 bucket 속 node들에 대해 node pool이 가능한지 알아봐야할 것 같다. 불가능하다면 자체 컨테이너를 만들어야할 것 같다.