상세 컨텐츠

본문 제목

Object pool

똑똑한 개발/C++ 게임개발

by 성댕쓰 2021. 10. 1. 22:17

본문

memory pool을 쓰면 메모리를 재사용하기 때문에 메모리 접근 오류가 있어도 한참 뒤에 문제가 발생하는 문제가 있다.

어떤 클래스에서 이런 문제가 발생했는지 확인하려면 상당히 많은 시간이 소요된다. Object pool은 이런 문제를 해결하기 위해 고안한 memory pool이다. 같은 Object는 같은 memory pool을 쓰도록 동작한다.

 

ObjectPool.h

#pragma once

#include "Types.h"
#include "MemoryPool.h"

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

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

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

 

사용은 다음처럼 하면된다.

Knight* knight = ObjectPool<Knight>::Pop();
ObjectPool<Knight>::Push(knight);

그런데 위와같은 방법은 delete 사용 xdelete사용 또는 해제를 잊어버리는 등 오류가 발생할 여지가 많다.

그래서 shared_ptr을 사용한다.

ObjectPool.h

template<typename Type>
class ObjectPool
{
public:
...

	static shared_ptr<Type> MakeShared()
	{
		shared_ptr<Type> ptr = { Pop(), Push };
		return ptr;
	}
...
};
shared_ptr<Knight> k = ObjectPool<Knight>::MakeShared();

 

MemoryPool class 수정하여 현재 얼마나 많은 Object가 예약, 사용되고 있는지 확인할 수 있다.

MemoryPool.h, MemoryPool.cpp

/*---------------------
	MemoryPool
---------------------*/

DECLSPEC_ALIGN(SLIST_ALIGNMENT)
class MemoryPool
{
...

private:
	...
	atomic<int32> _useCount = 0;
	atomic<int32> _reserveCount = 0;
};
...
void MemoryPool::Push(MemoryHeader* ptr)
{
    ptr->allocSize = 0;

    // Pool에 메모리 반납
    ::InterlockedPushEntrySList(&_header, static_cast<PSLIST_ENTRY>(ptr));

    _useCount.fetch_sub(1);
    _reserveCount.fetch_add(1);
}

MemoryHeader* MemoryPool::Pop()
{
    MemoryHeader* memory = static_cast<MemoryHeader*>(::InterlockedPopEntrySList(&_header));

    // 없으면 새로 만든다
    if (memory == nullptr)
    {
        memory = reinterpret_cast<MemoryHeader*>(::_aligned_malloc(_allocSize, SLIST_ALIGNMENT));
    }
    else
    {
        ASSERT_CRASH(memory->allocSize == 0);
        _reserveCount.fetch_sub(1);
    }

    _useCount.fetch_add(1);

    return memory;
}

 

앞에서 말했듯이 memory pool을 사용하면 메모리 오염이 일어나도 곧바로 에러가 발생하지 않는다.

StompAllocator를 활용할 수 있도록 코드를 수정해보자.

CoreMacro.h

 ...
 // 아래 Memory define을 모두 삭제
 /*----------------------
            Memory
    ----------------------*/
#ifdef _DEBUG
    #define x_alloc(size) PoolAllocator::Alloc(size)
    #define x_release(ptr) PoolAllocator::Release(ptr)
#else
    #define x_alloc(size) BaseAllocator::Alloc(size)
    #define x_release(ptr) BaseAllocator::Release(ptr)
#endif
...

Memory.h xnew, xdelete가 항상 PoolAllocator를 사용하도록 변경한다.

...
template<typename Type, typename... Args>
Type* xnew(Args&&... args)
{
	Type* memory = static_cast<Type*>(PoolAllocator::Alloc(sizeof(Type)));

	// placement new
	new(memory)Type(std::forward<Args>(args)...);
	return memory;
};

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

 

ObjectPool.h define에 따라 stomp를 사용할지 memory pool을 사용할지 선택할 수 있게 만든다.

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::AttchHeader(ptr, s_allocSize));
#else
		Type* memory = static_cast<Type*>(MemoryHeader::AttchHeader(s_pool.Pop(), s_allocSize));
#endif
		new(memory)Type(forward<Args>(args)...);
		return memory;
	}

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

...
};

Memory.cpp define에 따라 Stomp, memory pool 선택 가능하게 만든다.

...
void* Memory::Allocate(int32 size)
{
	MemoryHeader* header = nullptr;

	const int32 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::AttchHeader(header, allocSize);
}

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

	const int32 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
}
...

 

자주 사용하는 Types.h에 define을 정의한다.

...
#define _STOMP

xnew, xdelete 도 shared_ptr을 편하게 사용하게 하기 위해 다음을 정의한다

Memory.h

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

 

아래처럼 사용가능하다.

shared_ptr<Knight> k = ObjectPool<Knight>::MakeShared();
shared_ptr<Knight> k2 = MakeShared<Knight>();

1. memory pool 이 메모리 재사용하여 뒤늦게 오류가 발생되는 예시는?

- stomp allocator 사용하지 않으면 memory pool 사용하지 않더라도 메모리 오류 나옴.

- object pool은 동일한 object 끼리 pool 을 만들어 써서, 메모리 오류가 발생했을 때, 디버그 하기 쉬운 장점이 있다.

- object pool 만으로 메모리 오류를 막을 수 없다.

2. object pool 의 멤버변수가 static인 이유는?

- Type 별로 ObjectPool 한 개씩만 정의하여 사용하기 위함.

- template 이용하면 Type 별로 별개의 클래스가 정의됨.

3. object pool version 1이 문제가 되는 상황?

- 기본 delete 로 지우거나 Push 호출을 잊어버리는 상황.

4. memory.cpp 에 stomp allocator를 넣은 이유는?

- xnew, xdelete 가 PoolAllocator의 allocate, release를 호출함.

- xnew, xdelete 사용할 때도, define에 따라 stomp allocator를 사용할 수 있게 됨.

참조 : [C++과 언리얼로 만드는 MMORPG 게임 개발 시리즈] Part4: 게임 서버 - 인프런 | 강의 (inflearn.com)

'똑똑한 개발 > C++ 게임개발' 카테고리의 다른 글

소켓 프로그래밍 기초 #1  (0) 2021.11.03
TypeCast  (0) 2021.10.29
Memory pool #3  (0) 2021.09.30
Memory pool #2  (0) 2021.09.23
Memory pool  (0) 2021.09.15

관련글 더보기

댓글 영역