Memory pool을 사용하는 이유는 다음과 같다.
1. 이전에 사용한 memory를 사용하기 때문에, 커널단에 메모리 할당 코스트를 줄일 수 있다.
2. 작게 할당된 메모리가 많으면 나중에 메모리가 반환 되더라도 해당 메모리를 사용하지 못하는 경우가 생길 수 있는데 이런 경우를 줄일 수 있다.
방식은 할당 메모리를 queue에 넣고 요청하면 queue에서 빼서 주는 방식, 반환할 때는 다시 queue에 넣는다.
할당 메모리 사이즈 정보를 갖고 있는 메모리 헤더를 만들어 이를 queue에 관리한다.
MemoryPool.h
#pragma once
// [32]
/*---------------------
MemoryHeader
---------------------*/
struct MemoryHeader
{
// [MemoryHeader][Data] : 표준 new, delete도 같은 방식
MemoryHeader(int32 size) : allocSize(size) {}
static void* AttchHeader(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;
};
/*---------------------
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;
};
MemoryPool.cpp
#include "pch.h"
#include "MemoryPool.h"
MemoryPool::MemoryPool(int32 allocSize) : _allocSize(allocSize)
{
}
MemoryPool::~MemoryPool()
{
while (_queue.empty() == false)
{
MemoryHeader* header = _queue.front();
_queue.pop();
::free(header);
}
}
void MemoryPool::Push(MemoryHeader* ptr)
{
WRITE_LOCK;
ptr->allocSize = 0;
// Pool에 메모리 반납
_queue.push(ptr);
_allocCount.fetch_sub(1);
}
MemoryHeader* MemoryPool::Pop()
{
MemoryHeader* header = nullptr;
{
WRITE_LOCK;
// Pool에 여분이 있는지?
if (_queue.empty() == false)
{
header = _queue.front();
_queue.pop();
}
}
// 없으면 새로 만든다
if (header == nullptr)
{
header = reinterpret_cast<MemoryHeader*>(::malloc(_allocSize));
}
else
{
ASSERT_CRASH(header->allocSize == 0);
}
_allocCount.fetch_add(1);
return header;
}
MemoryPool을 사이즈 별로 관리하고 있는 자료구조를 만들어준다.
MemoryPool 사이즈가 작은 메모리일 수록 많이 가지고 있도록 만든다.
Memory.h
...
class MemoryPool;
/*-------------
Memory
-------------*/
class Memory
{
enum
{
// ~1024까지 32단위, ~2048까지 128단위, ~4096까지 256단위
POOL_COUNT = (1024 / 32) + (2048 / 128) + (4096 / 256),
MAX_ALLOC_SIZE = 4096
};
public:
Memory();
~Memory();
void* Allocate(int32 size);
void Release(void* ptr);
private:
vector<MemoryPool*> _pools;
// 메모리 크기 <-> 메모리 풀
// 시간복잡도 O(1) 위한 테이블
MemoryPool* _poolTable[MAX_ALLOC_SIZE + 1];
};
...
Memory.cpp
#include "pch.h"
#include "Memory.h"
#include "MemoryPool.h"
/*-------------
Memory
-------------*/
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::AttchHeader(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);
}
}
전역으로 사용할 수 있게 세팅해준다.
CoreGlobal.h, CoreGlobal.cpp
...
extern class Memory* GMemory;
...
#include "pch.h"
...
#include "Memory.h"
...
...
Memory* GMemory = nullptr;
...
class CoreGlobal
{
public:
CoreGlobal()
{
...
GMemory = new Memory();
...
}
~CoreGlobal()
{
...
delete GMemory;
...
}
} GCoreGlobal;
PoolAllocator를 만들어 사용할 수 있도록 세팅한다.
Allocator.h, Allocator.cpp
...
/*---------------------
PoolAllocator
---------------------*/
class PoolAllocator
{
public:
static void* Alloc(int32 size);
static void Release(void* ptr);
};
...
...
/*---------------------
PoolAllocator
---------------------*/
void* PoolAllocator::Alloc(int32 size)
{
return GMemory->Allocate(size);
}
void PoolAllocator::Release(void* ptr)
{
GMemory->Release(ptr);
}
...
CoreMacro.h
...
/*----------------------
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
...
아래 처럼 사용 가능하다.
int main()
{
for (int32 i = 0; i < 5; i++)
{
GThreadManager->Launch([]()
{
while (true)
{
Vector<Knight> v(10);
Map<int32, Knight> m;
m[100] = Knight();
this_thread::sleep_for(10ms);
}
});
}
GThreadManager->Join();
}
1. memoryPool 에서 pop할 때, MemoryHeader ptr을 return하는 이유는?
- 사용할 메모리 공간을 할당받고, 메모리 사이즈를 명세한 헤더를 앞에 붙여서 리턴하는 것.
- 사용하기 위해서는 memoryHeader 부분을 감안하여 계산후 사용해야 함. (Memory 클래스에서 이부분 처리함)
참조 : [C++과 언리얼로 만드는 MMORPG 게임 개발 시리즈] Part4: 게임 서버 - 인프런 | 강의 (inflearn.com)
Memory pool #3 (0) | 2021.09.30 |
---|---|
Memory pool #2 (0) | 2021.09.23 |
STL allocator (0) | 2021.09.12 |
Stomp allocator (0) | 2021.09.11 |
Allocator (0) | 2021.09.11 |
댓글 영역