게임 개발자를 위한 메모리 구조 3 – 게임 엔진은 왜 allocator를 직접 구현할까?

게임 개발을 하거나 게임 개발 공부를 하다보면, 힙에서 사용할 객체를 생성할 때 자연스럽게 new를 사용하게 됩니다.

사전에 객체 생성 시점을 정확하게 예측하기 어렵고, 언제 해제될지도 명확하지 않은 상황이라면 힙 메모리를 사용하는 것이 맞습니다.

new를 통해 메모리를 할당해서 사용하고, 적절한 위치에서 delete를 호출해 메모리를 해제하면 기본적으로 큰 문제는 없습니다.

일반적인 프로그램에서는 실제로 이런 방식만으로도 충분한 경우가 많습니다.

다시 말씀드리지만, new/delete 또는 스마트 포인터를 사용해서 메모리를 관리하는 방식 자체가 잘못된 것은 절대 아닙니다.

그런데 게임은 상황이 조금 다릅니다.

게임에서 사용되는 수많은 게임 객체들은 대부분 힙 메모리를 사용하게 되는데, 생성되고 제거되는 사이클이 굉장히 빈번합니다.

그리고 실제 게임 엔진 구조를 보다 보면, 생각보다 많은 엔진들이 자체 allocator 즉, 메모리 할당 관리자를 직접 구현해서 사용하는 것을 확인할 수 있습니다.

처음에는 “메모리를 직접 관리하는 게 굉장히 복잡할텐데 왜 이렇게까지 할까?”싶기도 합니다.

왜냐하면 C++은 이미 new, delete, STL allocator 같은 메모리 기능들을 제공하고 있기 때문입니다.

그런데 게임 엔진을 조금 더 깊게 살펴보면, 생각보다 많은 문제들이 결국 힙 할당(Heap Allocation)과 연결되어 있다는 것을 알게 됩니다.

그리고 이런 내용들을 인지한 상태에서 조금 더 깊이 들여다보기 시작하면,
allocator, memory pool, fragmentation 같은 개념들이 왜 중요한지 이해되기 시작합니다.


//
//

힙 할당은 생각보다 비용이 크다

게임 엔진에서 allocator와 memory pool을 사용하는 이유를 설명하는 메모리 구조 이미지
게임 엔진에서 Heap Allocation 비용, 메모리 파편화, memory pool 구조가 왜 중요한지 설명하는 이미지

힙 할당은 그냥 메모리를 하나 생성하는 작업처럼 느껴질 수도 있습니다.

아래 코드와 같이 Actor를 힙 공간에 생성하는 코드를 생각해보겠습니다.

Actor* actor = new Actor();

겉보기에는 단순히 객체 하나를 생성하는 코드처럼 보입니다.

그런데 실제 힙 할당은 생각보다 단순한 작업이 아닙니다.

운영체제나 런타임 메모리 관리자는 내부적으로 빈 공간을 찾고, Alignment를 맞추고, 메모리를 분할하거나 병합하는 작업들을 수행하게 됩니다.

우리는 단순히 new만 호출했을 뿐인데, 실제로는 내부에서 생각보다 다양한 작업들이 수행되고 있는 것입니다.

new를 사용하면 시스템이 이런 작업들을 알아서 처리해주기 때문에 크게 의식하지 못하고 넘어가는 경우가 많지만,
이런 작업들은 생각보다 비용이 꽤 큽니다.

특히 게임처럼 오브젝트 생성과 삭제가 매우 자주 발생하는 환경에서는 문제가 더 커집니다.

게임에서는 다양한 상황에서 게임 객체가 생성되고 제거되는 상황이 굉장히 자주 발생합니다.

총알이 생성되고 제거됩니다.

이펙트가 생성되고 제거됩니다.

파티클이 생성되고 제거됩니다.

몬스터가 생성되고 제거됩니다.

다시 말하면, 게임은 힙 할당을 굉장히 많이 사용하는 프로그램입니다.

메모리 최적화를 이야기할 때는 보통 “메모리를 얼마나 많이 사용하는가”에 집중하는 경우가 많습니다.

그런데 게임이라는 특수한 환경에서는 오히려 “메모리를 얼마나 많이 사용하는가”보다,
“메모리를 얼마나 자주 생성하고 삭제하는가”가 더 중요해지는 경우가 많습니다.

메모리를 할당하고 해제하는 데 드는 비용이 생각보다 꽤 크기 때문입니다.

그리고 이런 작업이 특정 순간에 몰리게 되면, 간헐적으로 끊김 현상(랙)이 발생하기도 합니다.


메모리를 반복해서 생성하고 삭제하면 어떤 일이 발생할까?

메모리를 계속 생성하고 삭제하는 상황을 조금 더 살펴보겠습니다.

처음에는 단순해 보입니다.

객체가 필요하면 생성하고, 필요 없어지면 제거하면 되기 때문입니다.

그런데 이런 작업이 오랫동안 반복되기 시작하면 메모리 공간이 점점 조각나기 시작합니다.

이걸 메모리 파편화(Fragmentation)라고 부릅니다.

예를 들어, 메모리 공간 중간 중간에 애매한 빈 공간들이 계속 생기는 상황입니다.

문제는 전체 메모리 용량은 충분한데도,
정작 연속된 큰 메모리 공간을 확보하지 못하는 상황이 발생할 수 있다는 점입니다.

그리고 이런 상태가 심해질수록 메모리 접근 효율도 점점 나빠질 수 있습니다.

특히 게임처럼 실시간 성능이 중요한 프로그램에서는 이런 문제가 굉장히 민감하게 작용합니다.


//

게임은 메모리 사용 패턴이 어느 정도 정해져 있다

그런데 재미있는 점은 게임은 일반적인 프로그램보다 메모리 사용 패턴이 어느 정도 예측 가능한 경우가 많다는 점입니다.

예를 들어, 총알은 대부분 짧은 시간 동안 생성되었다가 사라집니다.

파티클도 비슷합니다.

프레임 단위로 잠깐 사용되는 데이터들도 굉장히 많습니다.

즉, 게임은 메모리 사용 방식이 어느 정도 반복적인 패턴을 가지게 됩니다.

그래서 게임 엔진들은 이런 패턴에 맞춰 allocator를 직접 설계하기 시작했습니다.

총알 객체가 최대 1000개까지 생성될 수 있다면, 처음부터 일정량의 메모리를 미리 확보해두고 재사용하는 것입니다.

필요할 때마다 new/delete를 반복하는 것이 아니라, 미리 준비해둔 메모리를 계속 재사용하는 구조입니다.


Memory Pool의 등장

이런 이유 때문에 게임 엔진에서는 memory pool과같은 구조를 굉장히 많이 사용합니다.

개념 자체는 생각보다 단순합니다.

처음에 큰 메모리 공간을 한 번 확보해두고,
그 안에서 필요한 만큼 잘라서 사용하는 방식입니다.

예를 들면 아래와 같은 느낌입니다.

[        Pool Memory       ]

[Actor][Actor][Actor][Actor]

그리고 객체가 제거되더라도 실제로 메모리를 운영체제에 반환하는 것이 아니라,
“다시 사용 가능한 상태”로만 표시해둡니다.

그러면 다음 객체 생성 시 다시 빠르게 재사용할 수 있습니다.

이런 방식을 활용하면 힙 할당 자체를 반복적으로 수행하지 않아도 됩니다.

그리고 이 방법은 메모리 파편화 문제를 줄이는 데도 도움이 됩니다.


최근에는 Allocator가 더 중요해졌다

최근 게임 엔진 구조를 보면, allocator 이야기가 점점 더 중요해지고 있다는 느낌을 받게 됩니다.

ECS, Job System, Data Oriented Design 같은 구조들이 등장하면서 메모리를 어떻게 배치할 것인가 자체가 성능에 굉장히 큰 영향을 주기 시작했습니다.

예전에는 객체 구조 자체를 어떻게 설계할 것인가를 더 중요하게 보는 경우가 많았습니다.

그런데 최근에는 데이터를 얼마나 연속적으로 배치할 수 있는지,
그리고 Cache 효율을 얼마나 높일 수 있는지를 더 중요하게 보기 시작했습니다.

그리고 이 과정에서 allocator는 단순 메모리 관리 기능이 아니라,
엔진 성능 자체에 더 중요한 영향을끼치기 시작했습니다.


마무리

Allocator라는 개념이 그냥 메모리를 조금 더 빠르고 편리하게 관리하기 위한 기술처럼 느껴질 수도 있습니다.

저도 처음에는 그냥 new/delete 대신 사용하는 게 아닐까 정도로 생각했던 것 같습니다.

그런데 게임 엔진 구조를 조금 더 깊게 살펴보면, 생각보다 많은 문제들이 결국 Allocator와 연결된다는 것을 알게될 겁니다.

왜 memory pool을 사용하는지,
왜 ECS가 등장했는지,
왜 Cache Locality가 중요한지,
왜 엔진들이 자체 메모리 시스템을 구현하는지도 결국 같은 문제입니다.

그래서 최근 게임 엔진은 결국 객체를 어떻게 설계할 것인가보다,
데이터를 메모리에 어떻게 배치하고 재사용할 것인가를 더 중요하게 여기기 시작했습니다.

다음 글에서는 placement new가 왜 등장했는지,
그리고 allocator와 어떤 관계를 가지는지 조금 더 자세히 정리해보겠습니다.


👉 게임 엔진 구조를 더 깊이 이해하고 싶다면

아래 강의를 통해 직접 구현해보는 것을 추천드립니다.

C++로 만드는 게임 엔진 프레임워크 강의

👉 C++로 만드는 게임 엔진 프레임워크 강의 바로가기

//
   

댓글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다