Memory Pooling
카테고리: Server
인프런에 있는 루키스님의 게임 서버 강의를 듣고 정리한 내용입니다.
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 사용
댓글 남기기