KoreanFoodie's Study

[C++ 게임 서버] 2-3. Allocator 본문

Game Dev/Game Server

[C++ 게임 서버] 2-3. Allocator

GoldGiver 2023. 8. 23. 21:14

Rookiss 님의 '[C++과 언리얼로 만드는 MMORPG 게임 개발 시리즈] Part4: 게임 서버' 를 들으며 배운 내용을 정리하고 있습니다. 관심이 있으신 분은 꼭 한 번 들어보시기를 추천합니다!

[C++ 게임 서버] 2-3. Allocator

핵심 :

1. C++ 에서는 new 와 delete 도 오버로딩할 수 있는데, Allocator 를 만들어 커스텀화된 new 와 delete 를 사용해 보자!

2. placement new 기법을 이용하면, 내가 지정한 메모리에 객체를 초기화할 수 있다!

놀랍게도(?) C++ 에서는 new 와 delete 를 오버로딩하여, 메모리 관리를 섬세하게 커스터마이징할 수 있다.

static void* operator new(size_t size)
{
    cout << "Knight new! " << size << endl;
    void* ptr = ::malloc(size);
    return ptr;
}

static void operator delete(void* ptr)
{
    cout << "Knight delete!" << endl;
    ::free(ptr);
}

위 함수는 Knight 클래스의 new 와 delete 를 오버로딩 한 예시이다!

 

그런데, 사실 위와 같은 방식으로 new 와 delete 를 매번 오버로딩하기 보다는, 할당자(Allocator) 의 역할을 하는 클래스를 따로 빼서 관리하는 게 더 낫다. 바로 다음과 같이 말이다...

BaseAllocator 클래스 정의 :

/*-------------------
	BaseAllocator
-------------------*/

class BaseAllocator
{
public:
	static void*	Alloc(int32 size);
	static void	Release(void* ptr);
};

void* BaseAllocator::Alloc(int32 size)
{
	return ::malloc(size);
}

void BaseAllocator::Release(void* ptr)
{
	::free(ptr);
}

음..? 일단 겉보기에는, 딱히 하는 역할이 없어 보인다. 

 

일단 더 간편하게 사용할 수 있도록 매크로도 정의할 것인데...

/*----------------
	  Memory
-----------------*/

#ifdef _DEBUG
#define xalloc(size)		BaseAllocator::Alloc(size)
#define xrelease(ptr)		BaseAllocator::Release(ptr)
#else
#define xalloc(size)		BaseAllocator::Alloc(size)
#define xrelease(ptr)		BaseAllocator::Release(ptr)
#endif

 

사실 Allocator 를 따로 두는 이유는, 보통 메모리를 더 효율적으로 사용하기 위해서 이다.

우리는 new 와 delete 를 빈번하게 하게 되면 '메모리 파편화' 현상이 일어나는 것을 알고 있는데, 추후 커스터마이징된 Allocator 를 이용해서 그러한 현상을 해결할 것이다. 😊

 

일단 메모리 할당 및 해제를 간편하게 할 수 있는, 아래 핵심 코드를 보자.

template<typename Type, typename... Args>
Type* xnew(Args&&... args)
{
	Type* memory = static_cast<Type*>(xalloc(sizeof(Type)));
	new(memory)Type(forward<Args>(args)...); // placement new
	return memory;
}

template<typename Type>
void xdelete(Type* obj)
{
	obj->~Type();
	xrelease(obj);
}

위의 xnew 를 보면... 먼저 메모리를 할당하고, 생성자를 호출한 다음, 할당했던 메모리를 리턴한다.

그런데 신기한 코드가 있다. 바로...

new(memory)Type(forward<Args>(args)...); // placement new

위 코드인데, 이는 placement new 라는 기능으로, 내가 원하는 메모리에 객체를 초기화해 준다! (좀 더 자세한 내용은 이 글 참고)

즉, 위의 xnew 는 객체의 크기만큼 메모리를 할당하고, 가변 길이 템플릿을 받아서 생성자를 호출해 준다. 😉 (소멸자도 동작 원리는 비슷하다)

 

앞으로 위의 xnew 와 xdelete 를 다음과 같이 유용하게 사용할 것이다 🙂

Knight* knight = xnew<Knight>(100);

xdelete(knight);

 

음... 혹시 위처럼 메모리를 할당하고, 생성자를 호출하는 프로세스를 나눠서 해도 되는지 의문이 들 수도 있는데...

사실 그냥 new 를 호출해도, 할당과 생성자 호출을 한번에 하지 않고 나눠서 한다 😁

잘 보면, new Knight() 를 할 때, 메모리를 먼저 할당하고(빨간색), 그 다음 생성자를 호출하여 이것 저것 작업(파란색)을 하는 것을 확인할 수 있다 🤣

Comments