Memory Pooling

Date:     Updated:

카테고리:

태그:

인프런에 있는 루키스님의 게임 서버 강의를 듣고 정리한 내용입니다.


Memory Pool

메모리 풀링을 사용하는 이유는 크게 두 가지 이다. 첫 번째로 new/delete 방식은 커널 영역으로 넘어가 작동하는 방식이기 때문에 오버헤드가 발생한다( + 무조건 생성자/소멸자 호출). 따라서 메모리 할당/해제가 매우 빈번하게 일어나는 상황에서는 한 번에 큰 메모리를 할당받고 그 안에서 수동으로 관리하는 방식을 생각할 수 있다. 다음으로는 메모리 파편화 문제이다. 메모리 할당/해제를 반복적으로 수행하다 보면 할당된 메모리가 파편적으로 존재해 연속적인 메모리 할당이 어려워질 수 있다. 이러한 이유들로 메모리 풀링을 사용하는데, 최근에는 최적화가 잘 되어있어서 굳이 사용하지 않는 경우도 많다고 한다.


메모리 풀링에서 생각할 수 있는 관리 방법은 크게 두 가지이다.

  • 데이터 타입에 상관 없이 크기에 따라 쪼개서 사용
  • 동일한 데이터 타입에 대해서만 풀링

당연한 이야기지만, 후자의 경우가 관리도 편리하고 디버깅도 쉬워진다. 이에 대한 내용은 오브젝트 풀링에서 따로 다루고, 이번 포스팅에서는 전자에 대한 내용을 다룬다.


MemoryPool

/*-----------------
	MemoryHeader
------------------*/

struct MemoryHeader
{
	// [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 : 필요한 추가 정보
};

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

class MemoryPool
{
public:
	MemoryPool(int32 allocSize);
	~MemoryPool();

	void			Push(MemoryHeader* ptr);
	MemoryHeader*	Pop();


private:
	int32 _allocSize = 0;
	atomic<int32> _allocCount = 0;

	USE_LOCK;
	queue<MemoryHeader*> _queue; // 데이터 담을 수 있으면 크게 상관 없음
};
  • 메모리 풀에 들어있는 데이터가 어떤 타입인지 모르기 때문에 데이터 앞에 헤더가 붙어있음
    • 사실 위 이유 말고도, new로 할당해도 메모리 까보면 헤더에 이런저런 정보들 들어있다.
  • placement new를 사용해 메모리를 힙 영역에 새로 할당하지 않고, 기존 header 메모리 재사용
  • 포인터 연산을 통해 헤더or데이터 반환
  • 멀티쓰레드 상황을 고려해 lock 이용


Memory Manager

// Memory.h
class Memory
{
	enum
	{
		// ~1024까지 32단위, ~2048까지 128단위, ~4096까지 256단위
		POOL_COUNT = (1024 / 32) + (1024 / 128) + (2048 / 256),
		MAX_ALLOC_SIZE = 4096
	};

public:
	Memory();
	~Memory();

	void*	Allocate(int32 size);
	void	Release(void* ptr);

private:
	vector<MemoryPool*> _pools;

	// 메모리 크기 <-> 메모리 풀
	// 풀 빠르게 찾기 위한 테이블
	MemoryPool* _poolTable[MAX_ALLOC_SIZE + 1];
};


// Memory.cpp
Memory::Memory()
{
	int32 size = 0;
	int32 tableIndex = 0;

	for (size = 32; size <= 1024; size += 32)
	{
		MemoryPool* pool = new MemoryPool(size);
		_pools.push_back(pool);

		while (tableIndex <= size)
		{
			_poolTable[tableIndex] = pool;
			tableIndex++;
		}
	}

	for (; size <= 2048; size += 128)
	{
		MemoryPool* pool = new MemoryPool(size);
		_pools.push_back(pool);

		while (tableIndex <= size)
		{
			_poolTable[tableIndex] = pool;
			tableIndex++;
		}
	}

	for (; size <= 4096; size += 256)
	{
		MemoryPool* pool = new MemoryPool(size);
		_pools.push_back(pool);

		while (tableIndex <= size)
		{
			_poolTable[tableIndex] = pool;
			tableIndex++;
		}
	}
}

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

	_pools.clear();
}

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

	if (allocSize > MAX_ALLOC_SIZE)
	{
		// 메모리 풀링 최대 크기를 넘어가면 일반 할당
		header = reinterpret_cast<MemoryHeader*>(::malloc(allocSize));
	}

	else
	{
		// 메모리 풀에서 꺼내오기
		header = _poolTable[allocSize]->Pop();
	}

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

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

	const int32 allocSize = header->allocSize;
	ASSERT_CRASH(allocSize > 0);

	if (allocSize > MAX_ALLOC_SIZE)
	{
		// 메모리 풀링 최대 크기를 넘어가면 일반 해제
		::free(header);
	}

	else
	{
		// 메모리 풀에 반납
		_poolTable[allocSize]->Push(header);
	}
}
  • 4096바이트를 넘어가는 데이터는 굳이 풀링으로 관리하지 않고, 별도로 메모리 할당/해제
  • ~1024까지 32단위, ~2048까지 128단위, ~4096까지 256단위로 메모리 풀링
    • 즉, 1~32 바이트 데이터는 MemoryPool(32)에서 관리
    • 33~64 바이트 데이터는 MemoryPool(64)에서 관리, …
    • 작은 데이터일 수록 촘촘히 풀링
  • MemoryPool을 O(1)으로 빠르게 찾기 위해 PoolTable 사용



맨 위로 이동하기

Server 카테고리 내 다른 글 보러가기

댓글 남기기