게임 개발자를 위한 메모리 구조 4 – placement new는 왜 등장했을까?

이번에는 placement new라는 문법에 대해 살펴보려고 합니다.

placement new는 일반적으로 사용하는 new와 문법에서 다소 차이가 있습니다.

사용 방법은 아래와 같습니다.

new (memory) Actor();

new와 타입 사이에 메모리 주소가 들어가는 특이한 형태의 문법입니다.

저도 처음 접했을 때는 “new를 사용하는데 앞에 메모리 주소가 들어가네?”, “어디에 사용하지?”하는 생각이 들었던 것 같습니다.

그리고 “그냥 new를 사용해서 객체를 생성하면 되는 것 아닌가?”라고 생각했던 것 같습니다.

일반적인 프로그램에서는 대부분 메모리 할당이 필요할 때 new를 사용해서 메모리 공간을 확보해도 큰 문제가 없습니다.

그런데 게임 엔진이나 메모리 시스템을 조금 더 깊게 알아가면,
생각보다 많은 부분에서 placement new를 잘 활용하면 메모리 사용성 측면에서 성능을 개선할 수 있다는 것을 알 수 있습니다.

특히 allocator, memory pool, custom container 같은 시스템을 구현하기 시작하면 높은 확률로 Placement new가 등장하게 됩니다.

그리고 이 시점부터 “메모리 할당”과 “객체 생성”은 서로 다른 작업이라는 사실을 조금씩 이해하게 됩니다.


//
//

우리는 new를 하나의 작업처럼 생각한다

아래 코드는 일반적으로 힙 메모리를 사용하는 객체를 생성하는 코드입니다.
이 코드를 볼 때 우리는 단순히 하나의 객체를 힙 공간에 생성하는 코드라고 이해합니다.

Actor* actor = new Actor();

처음 프로그래밍을 시작하는 시점에서는 이 정도로만 이해해도 문제가 없는 경우가 많습니다.

그런데 new는 C언어의 malloc이 발전된 형태입니다.
그리고 내부적으로는 생각보다 더 많은 작업이 수행됩니다.

C언어의 malloc은 우리가 요청한 만큼의 메모리 공간을 힙 영역에 할당합니다.

그런데 new는 사실 아래 두 작업을 한 번에 수행합니다.

  1. 메모리를 할당합니다 ( 메모리 확보 )
  2. 그 위치에 객체를 생성합니다 ( 생성자 호출 )

처음에는 이 둘을 굳이 분리해서 생각해야 할 경우가 거의 없습니다.

그런데 allocator나 memory pool 같은 구조를 만들기 시작하면 이야기가 조금 달라집니다.


게임 엔진은 메모리를 미리 확보해두는 경우가 많다

이전 글에서도 이야기했지만, 게임은 객체 생성과 삭제가 굉장히 빈번한 프로그램입니다.

총알이 생성되고 제거됩니다.
파티클이 생성되고 제거됩니다.
이펙트가 생성되고 제거됩니다.

그리고 이런 작업이 프레임마다 계속 반복됩니다.

그래서 게임 엔진은 매번 힙 할당(Heap Allocation)을 수행하기보다,
처음부터 일정량의 메모리를 미리 확보해두고 재사용하는 방식을 굉장히 많이 사용합니다.

이를 간략히 코드로 표현해보면 아래와 같습니다.

아래 코드는 1024바이트의 크기로 힙 메모리 공간을 할당하는 코드입니다.

char* memory = new char[1024];

이렇게 메모리 공간을 미리 확보해둡니다.

그런데 여기서 문제가 하나 생깁니다.

메모리는 확보했는데, 아직 객체는 생성되지 않았다는 점입니다.

즉 메모리 공간만 존재할 뿐, 실제 Actor 객체는 아직 만들어지지 않은 상태입니다.

따라서 메모리 공간을 확보해두고, 이 공간을 재사용하려면 미리 확보해둔 메모리 공간에 정확하게 객체를 생성해야 합니다.

지금까지 사용했던 new 연산자로는 이 작업이 불가능합니다. new를 사용하면 새로운 힙 공간이 할당되기 때문입니다.

이때 필요한 기술이 바로 placement new입니다.


//

placement new는 이미 존재하는 메모리 위에 객체를 생성한다

placement new와 allocator, memory pool 구조를 설명하는 게임 엔진 메모리 관리 이미지
placement new가 객체 생성과 메모리 할당을 어떻게 분리하는지 설명하는 게임 엔진 메모리 구조 이미지

placement new는 말 그대로 이미 존재하는 메모리 위치에 객체를 생성하는 문법입니다.

아래 코드를 살펴보겠습니다.

char* memory = new char[sizeof(Actor)];

Actor* actor = new (memory) Actor();

위의 코드에서 actor 객체를 생성할 때 Placement new를 사용했습니다.

이때 actor를 생성하는 과정에서 힙 할당을 새로 수행하지 않습니다.

이미 확보되어 있는 memory 위치에 Actor 객체를 생성해 배치하는 코드입니다.

placement new는 “메모리 확보”가 아니라, “생성자 호출”만 수행합니다.

이 차이가 굉장히 중요합니다.

처음에는 단순히 특이한 문법처럼 느껴질 수도 있지만,
실제로는 게임 엔진 메모리 구조에서 굉장히 핵심적인 역할을 하게 됩니다.


allocator와 memory pool은 결국 placement new와 연결된다

이전 글에서 설명한 Allocator나 메모리 풀은 대체로 아래와 같은 방식으로 동작하도록 구현합니다.

  1. 처음에 필요한 만큼의 큰 메모리 공간을 확보
  2. 필요할 때마다 미리 확보해둔 공간을 잘라서 사용
  3. 이 때 미리 확보해둔 영역에 객체를 생성해 배치하기 위해 Placement new를 사용

이 과정을 살펴보면, 메모리 확보(관리)와 객체 생성은 분리되어 있다는 것을 알 수 있습니다.

예전에는 “객체를 생성하면 메모리도 같이 생성된다” 정도로만 생각했다면,
이제부터는 “메모리는 이미 존재하고 객체만 나중에 생성될 수도 있다”는 관점으로 확장될 수 있습니다.

그리고 이 구조를 이해하기 시작하면, 게임 엔진이 왜 allocator를 직접 구현하는지,
왜 memory pool을 사용하는지, 왜 placement new가 필요한지도 자연스럽게 이해할 수 있게 됩니다.


placement new를 사용하면 destroy는 직접 호출해야 한다

placement new를 사용할 때 주의해야할 점이 있습니다.

앞서 살펴본 대로 미리 메모리 공간을 확보해두고, placement new를 활용해 객체를 생성하면서 미리 확보해둔 공간에 배치를 했습니다.

placement new는 일반적인 new와 다르게 메모리를 새로 할당하지 않습니다.

그리고 메모리 해제도 자동으로 처리되지 않습니다.

그래서 객체를 제거할 때는 아래 코드와 같이 destructor를 직접 호출해야 합니다.

actor->~Actor();

new로 객체를 생성하면, 메모리 공간을 새로 할당하고 객체의 생성자를 호출한다고 설명했습니다.

placement new 방식은 미리 생성해둔 공간에 객체를 생성하면서 배치하는 구조입니다. 이 과정에서 객체의 생성자가 호출됩니다.

이와 비슷하게 delete는 new로 할당한 메모리 공간을 해제하면서 소멸자까지 호출하는 과정이 처리됩니다.

그런데 placement new로 객체를 생성하면, 미리 확보해둔 영역에 배치하는 것이기 때문에 “메모리 해제”는 빠져야 합니다.

따라서 delete가 처리해주는 과정 중에서 “메모리 공간 해제”는 제외하고 “소멸자 호출”만 남겨야 합니다.
이런 이유로 소멸자를 명시적으로 호출해줘야 합니다.

소멸자를 직접 호출해야 한다니…처음 보면 굉장히 낯설게 느껴질 수도 있습니다.

그런데 placement new는 메모리와 객체 생성을 분리한 구조이기 때문에, 객체 제거 역시 직접 관리해야 합니다.

placement new를 사용하기 시작하는 시점부터는 메모리 관리 자체를 개발자가 직접 제어하기 시작한다고 볼 수도 있습니다.


최근 엔진은 결국 메모리 이야기로 다시 연결된다

게임 엔진 구조를 조금 더 깊게 보다 보면, 생각보다 많은 구조들이 결국 메모리 이야기로 다시 연결됩니다.

ECS,
Data Oriented Design,
Job System,
GPU Driven Rendering 등등의 키워드는 결국 데이터를 어떻게 배치하고 관리할 것인가를 고민한 흔적입니다.

그리고 이 과정에서 allocator, memory pool, placement new 같은 구조들이 자연스럽게 등장하게 됩니다.

처음에는 단순히 어려운 문법처럼 느껴질 수도 있습니다.
하지만 “메모리 확보”와 “객체 생성”을 분리하기 위한 구조라는 것을 이해하기 시작하면, placement new가 왜 존재하는지 조금씩 이해되기 시작할 것입니다.


마무리

placement new라는 문법이 굉장히 특이하고 복잡하게 느껴질 수도 있습니다.

“왜 이렇게까지 복잡하게 객체를 생성해야 하지?”하는 생각이 들었던 적도 있었던 것 같습니다.

그런데 시간이 지날수록, 엔진 코드가 점점 더 이해될수록, 그 코드를 구현한 개발자가 고민한 이유가 느껴지기 시작했습니다.

여러분도 어느 순간 생각보다 많은 곳에서 placement new를 잘 활용하면 메모리 측면의 효율을 높일 수 있다는 점을 이해하리라 생각합니다.

왜 allocator를 구현하는지,
왜 memory pool을 사용하는지,
왜 메모리 재사용이 중요한지,
왜 객체 생성과 메모리 할당을 분리하려고 하는지도 결국 모두 연결됩니다.

그리고 이는 최근 게임 엔진은 결국 객체 자체보다,
메모리를 어떻게 배치하고 재사용할 것인가를 점점 더 중요하게 고려한다는 점과도 연결됩니다.


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

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

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

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

//
   

댓글 남기기

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