KoreanFoodie's Study

[C++ 게임 서버] 2-9. Object Pool 본문

Game Dev/Game Server

[C++ 게임 서버] 2-9. Object Pool

GoldGiver 2023. 9. 15. 11:39

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

[C++ 게임 서버] 2-9. Object Pool

핵심 :

1. ObjectPool 을 사용하면, 각 타입별로 풀을 만들게 되어, 메모리 오염 이슈가 발생했을 때 문제의 원인을 파악하기가 쉬워진다.

2. 커스텀한 삭제자를 객체 메모리를 풀에 반납하는 형식을 넣은 MakeShared 를 활용하면, ObjectPool 을 사용하는 스마트 포인터를 쉽게 만들 수 있다.

3. Allocator 를 StompAllocator 로 쉽게 교체할 수 있도록 #define 문법을 사용하면, 테스트용으로 StompAllocator 를 쉽게 적용 가능하다.

우리는 이전까지 메모리 풀에 대해 알아보았는데, 메모리 풀은 메모리 크기에 따라 객체들이 알아서 풀을 같이 사용한다.

집단 생활이 으레 그렇듯, 기숙사 생활에서도 문제가 생기는데 메모리 풀에서도 문제가 생기지 않을리가 없다 😂 특히 메모리풀을 이용했는데 메모리 오염이 발생하게 되면, 어떤 객체에서 문제가 발생했는지 쉽게 파악하기가 어려울 때가 있다.

그래서 객체의 타입별로 풀을 만드는 것이 좋다. 그리하여, 오늘 다룰 내용은 ObjectPool 되시겠다. 코드를 보자.

template<typename Type>
class ObjectPool
{
public:
	template<typename... Args>
	static Type* Pop(Args&&... args)
	{
		Type* memory = static_cast<Type*>(MemoryHeader::AttachHeader(s_pool.Pop(), s_allocSize));
		new(memory)Type(forward<Args>(args)...); // placement new
		return memory;
	}

	static void Push(Type* obj)
	{
		obj->~Type();
		s_pool.Push(MemoryHeader::DetachHeader(obj));
	}

	static shared_ptr<Type> MakeShared()
	{
		shared_ptr<Type> ptr = { Pop(), Push };
		return ptr;
	}

private:
	static int32		s_allocSize;
	static MemoryPool	s_pool;
};

template<typename Type>
int32 ObjectPool<Type>::s_allocSize = sizeof(Type) + sizeof(MemoryHeader);

template<typename Type>
MemoryPool ObjectPool<Type>::s_pool{ s_allocSize };

 

 

 

일단, 이제 오브젝트 풀 별로 타입이 각각 다를 것이다. 멤버 변수에 s_allocSize 와 s_pool 이 있는데, 각각 타입의 사이즈와 사용할 메모리 풀을 의미한다. 이 두 녀석은, 아래 부분에서 템플릿 클래스들이 생성(Generate) 될 때, 초기화 될 것이다.

 

이제 함수를 보자.

Pop 을 보면, 풀에 사용 가능한 녀석이 있으면 꺼내서 쓰고, 아니면 메모리를 새로 할당할 것임을 알 수 있다. 사실 Push 의 경우도 기존에 메모리 풀을 사용했던 방식과 비슷하다.

AttachHeader 와 DetachHeader 가 무엇이었는지 기억이 가물가물할 수 있는데, 사실 그냥 Header 의 위치를 타입의 포인터 사이즈 만큼 증가시키거나 감소시키면서, 메모리를 읽는 게 다다 😉

DECLSPEC_ALIGN(SLIST_ALIGNMENT)
struct MemoryHeader : public SLIST_ENTRY
{
	// [MemoryHeader][Data]
	MemoryHeader(int32 size) : allocSize(size) { }

	static void* AttachHeader(MemoryHeader* header, int32 size)
	{
		new(header)MemoryHeader(size); // placement new
		return reinterpret_cast<void*>(++header);
	}

	static MemoryHeader* DetachHeader(void* ptr)
	{
		MemoryHeader* header = reinterpret_cast<MemoryHeader*>(ptr) - 1;
		return header;
	}

	int32 allocSize;
	// TODO : 필요한 추가 정보
};

 

마지막으로, MakeShared 를 보자.

static shared_ptr<Type> MakeShared()
{
    shared_ptr<Type> ptr = { Pop(), Push };
    return ptr;
}

이 녀석은 왜 필요할까?

우리가 만든 ObjectPool 을 이용한 스마트 포인터를 만들때, c++ 에서 정의된 make_shared 를 사용하면 커스텀 삭제자를 사용할 수 없는 문제가 있다.

그래서 커스텀 삭제자에 Push 로직을 넣기 위해, make_shared 느낌으로 간편하게 사용할 Wrapper 함수를 만든 것이다 😁

 

이제 위의 오브젝트 풀은 다음과 같이 사용할 수 있다 :

class Knight
{
public:
	int32 _hp = rand() % 1000;
};

int main()
{
	Knight* knights[100];

	// 생성
	for (int32 i = 0; i < 100; i++)
		knights[i] = ObjectPool<Knight>::Pop();

	// 반납
	for (int32 i = 0; i < 100; i++)
	{
		ObjectPool<Knight>::Push(knights[i]);
		knights[i] = nullptr;
	}
	
	shared_ptr<Knight> sptr = ObjectPool<Knight>::MakeShared();
}

보면, Knight 객체를 오브젝트 풀 방식으로 100개 만들고, 다시 반납을 하고 있다.

ObjectPool 의 MakeShared 함수를 이용하면, 명시적인 delete 가 필요없는 스마트 포인터도 간단하게 만들 수 있다.

 

참고로, 다음과 같이 PoolAllocator 대신 StompAllocator 를 사용하여 Push 와 Pop 을 진행할 수도 있다 :

#ifdef _STOMP
		MemoryHeader* ptr = reinterpret_cast<MemoryHeader*>(StompAllocator::Alloc(s_allocSize));
		Type* memory = static_cast<Type*>(MemoryHeader::AttachHeader(ptr, s_allocSize));
#else
		Type* memory = static_cast<Type*>(MemoryHeader::AttachHeader(s_pool.Pop(), s_allocSize));
#endif		
		new(memory)Type(forward<Args>(args)...); // placement new
		return memory;
	}

	static void Push(Type* obj)
	{
		obj->~Type();
#ifdef _STOMP
		StompAllocator::Release(MemoryHeader::DetachHeader(obj));
#else
		s_pool.Push(MemoryHeader::DetachHeader(obj));
#endif
	}

#define 만 잘 해주면, 테스트용으로 StompAllocator 를 사용할 수 있게 될 것이다 😎

 

마지막으로, 다음과 같이 MemoryPool 에 대한 공용 MakeShared 함수를 만들어 주면...

template<typename Type>
shared_ptr<Type> MakeShared()
{
	return shared_ptr<Type>{ xnew<Type>(), xdelete<Type> };
}

 

MemoryPool 에 대해서도, MakeShared 를 아래 버전처럼 사용할 수도 있을 것이다!

// ObjectPool 을 위한 MakeShared
shared_ptr<Knight> sptr = ObjectPool<Knight>::MakeShared();

// MemoryPool 을 위한 MakeShared
shared_ptr<Knight> sptr2 = MakeShared<Knight>();
Comments