Coding Memo

Object Pool 본문

Game Server (C++)

Object Pool

minttea25 2022. 10. 11. 12:10

본 포스팅은 인프런에 등록되어 있는 Rockiss 님의 강의를 보고 간단하게 정리한 글입니다.


동일한 클래스를 모아서 관리하는 Object Pool을 구현

메모리 풀과 비슷한 방식으로 작동한다. 그러나 메모리 크기에 따라 공간을 나누었던 메모리 풀과 다르게 타입 하나에 하나의 풀을 할당하여 사용할 것이다.

 

메모리 관련 오류가 발생했을 때 어떤 클래스에서 오류가 난 것인지 알 수 있는 장점이 있다.


Object Pool

private 맴버로 할당 크기와 타입별 사용할 메모리 풀을 static으로 선언한다.

template<typename Type>
class ObjectPool
{
public:
	static Type* Pop(Args&&... args) {}
    static void Push(Type* obj) {}
private:
	static int s_allocSize;
	static MemoryPool s_pool;
};

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

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

 

참고: static 맴버라서 하나만 존재하는 것은 맞지만 template를 적용했기 때문에 Type마다, 즉 클래스마다 하나씩 가지고 있을 수 있다. (각 타입마다 static 맴버를 가지고 있다고 생각하자)

 

Pop을 할때는 해당 클래스를 생성하는 것이므로 생성자를 호출하면 된다.

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;
}

 

Push도 마찬가지로 Pool에 자원을 반납하는 형태로, 소멸자를 호출하고 Push를 하면 된다.

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

 

실제 활용을 할때는 ObjectPool<>로 활용하면 되는데, ObjectPool 방식으로 생성하고 나중에 xxdelete나 delete로 직접 삭제하여 문제가 발생할 가능성이 존재한다.

Unit* unit = ObjectPool<Unit>::Pop();
ObjectPool<Unit>::Push(unit);

 

위 문제에 대해 shared_ptr을 사용하는데(RefCounting 참고), 메모리 할당방식과 메모리 해제 방식을 직접 지정하는 방법을 이용한다.

memory에 보면 객체 생성방법과 삭제방법을 지정하는 shared_ptr 생성자가 있다.

template <class _Ux, class _Dx,
        enable_if_t<conjunction_v<is_move_constructible<_Dx>, _Can_call_function_object<_Dx&, _Ux*&>,
                        _SP_convertible<_Ux, _Ty>>,
            int> = 0>
    shared_ptr(_Ux* _Px, _Dx _Dt) { // construct with _Px, deleter
        _Setpd(_Px, _STD move(_Dt));
    }

 

다음의 함수를 ObjectPool에 추가한다.

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

 

"ObjectPool.h"

#pragma once

#include "MemoryPool.h"

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 int s_allocSize;
	static MemoryPool s_pool;
};

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

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

 

테스트 코드

MemoryPool에서 useCount와 reserveCount를 확인해보도록 하자.

(reserveCount는 Pool에 해당 객체의 개수이다.)

class Unit
{
public:
	Unit()
	{
		cout << "Unit()\n";
	}
	~Unit()
	{
		cout << "~Unit()\n";
	}
	int _hp = rand() % 1000;
};

int main()
{
	/*Unit* units[100];

	for (int i = 0; i < 100; i++)
	{
		units[i] = ObjectPool<Unit>::Pop();
	}

	for (int i = 0; i < 100; i++)
	{
		ObjectPool<Unit>::Push(units[i]);
		units[i] = nullptr;
	}*/

	shared_ptr<Unit> sptr = ObjectPool<Unit>::MakeShared();
}

무조건적으로 xxnew를 할때 StompAllocator를 사용할 수 없으므로, STOMP를 정의했을 때만 StompAllocator를 사용하도록 하고 그외는 MemoryPool을 적용시켜보도록 하자.

더보기

CoreMacro에서 xxalloc과 xxrelease 매크로를 삭제하고 무조건적으로 PoolAllocator를 사용하도록 하기 위해

Memory.h 에서 xxnew와 xxdelete 실행 시 삭제한 매크로 대신 PoolAllocator를 사용하자.

 

또한 ObjectPool을 사용하지 않고 shared_ptr을 생성할 수도 있기 때문에 이에 맞는 MakeShared() 함수도 작성한다.

 

'Memory.h"

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

template<typename Type>
void xxdelete(Type* obj)
{
	obj->~Type();
	PoolAllocator::Release(obj);
}

template<typename Type>
shared_ptr<Type> MakeShared()
{
	return shared_ptr<Type>{ xxnew<Type>(), xxdelete<Type> };
}

 

이제 STOMP 사용을 하게 되면 ObjectPool에서 Pool을 사용하지 않고 StompAllocator를 사용하고, 그렇지 않다면 기존 ObjectPool 방식으로 pool을 이용한 메모리 할당을 이용하도록 변경한다.

 

"ObjectPool.h"

#pragma once

#include "MemoryPool.h"

template<typename Type>
class ObjectPool
{
public:
	template<typename... Args>
	static Type* Pop(Args&&... args)
	{
#ifdef _STOMP
		MemoryHeader* ptr = reinterpret_cast<MemoryHeader*>(StompAllocator::Alloc(s_allocSize));
		Type* memory = static_cast<Type*>(MemoryHeader::AttachHeader(s_pool.Pop(), 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
	}

	static shared_ptr<Type> MakeShared()
	{
		shared_ptr<Type> ptr = { Pop(), Push };
		return ptr;
	}
private:
	static int s_allocSize;
	static MemoryPool s_pool;
};

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

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

 

ObjectPool에서 pool을 사용하지 않는 방식으로 되어있으면 Memory도 바꿔주어야 하므로 STOMP가 활성화되어 있을때, pool을 사용하지 않고 StompAllocator를 이용하는 방식으로 바꾼다.

#include "pch.h"
#include "Memory.h"
#include "MemoryPool.h"

Memory::Memory()
{
	int size = 0;
	int tableIndex = 0;

	// 참조 구역 나누기
	...
}

Memory::~Memory()
{
	for (MemoryPool* pool : _pools)
	{
		delete pool;
	}

	_pools.clear();
}

void* Memory::Allocate(int size)
{
	MemoryHeader* header = nullptr;
	const int allocSize = size + sizeof(MemoryHeader);

#ifdef _STOMP
	header = reinterpret_cast<MemoryHeader*>(StompAllocator::Alloc(allocSize));
#else
	if (allocSize > MAX_ALLOC_SIZE)
	{
		// 메모리 풀링 최대 크기 벗어나면 일반 할당
		header = reinterpret_cast<MemoryHeader*>(::_aligned_malloc(allocSize, SLIST_ALIGNMENT));
	}
	else
	{
		// 메모리 풀에서 꺼내온다
		header = _poolTable[allocSize]->Pop();
	}
#endif

	return MemoryHeader::AttachHeader(header, allocSize);
}

void Memory::Release(void* ptr)
{
	MemoryHeader* header = MemoryHeader::DetachHeader(ptr);

	const int allocSize = header->allocSize;
	ASSERT_CRASH(allocSize > 0);
#ifdef _STOMP
	StompAllocator::Release(header);
#else
	if (allocSize > MAX_ALLOC_SIZE)
	{
		// 메모리 풀링 최대 크기를 벗어나면 일반 해제
		::_aligned_free(header);
	}
	else
	{
		// 메모리 풀에 반납한다
		_poolTable[allocSize]->Push(header);
	}
#endif // _STOMP
}

 

이렇게 코드를 작성할때는 ObjectPool, MemoryPool을 이용하지만 Stomp가 활성화 되어있을 때는 StompAllocator를 이용하도록 바꾸었다.


참고: #ifdef _STOMP를 활성화 시키려면 "#define _STOMP"를 추가해 주면 된다.

'Game Server (C++)' 카테고리의 다른 글

Socket Programming Basic  (0) 2022.10.18
Type Cast  (1) 2022.10.11
Memory Pool  (0) 2022.10.09
메모리 할당 - STL Allocator  (0) 2022.10.09
메모리 할당 - Stomp Allocator  (0) 2022.10.06