MemoryPool.h
#pragma once
enum {
SLIST_ALIGHMENT = 16
};
DECLSPEC_ALIGN(SLIST_ALIGHMENT)
struct MemoryHeader: public SLIST_ENTRY { // Lock-Free
// [MemoryHeader][Data]
MemoryHeader(int32 size):_allocSize(size) {}
static void* AttachHeader(MemoryHeader* header, int32 size) {
new(header)MemoryHeader(size);
return reinterpret_cast<void*>(++header);
}
static MemoryHeader* DetachHeader(void* ptr) {
MemoryHeader* header = reinterpret_cast<MemoryHeader*>(ptr) - 1;
return header;
}
int32 _allocSize;
//TODO: 추가 정보
};
DECLSPEC_ALIGN(SLIST_ALIGHMENT)
class MemoryPool
{
public:
MemoryPool(int32 allocSize);
~MemoryPool();
void Push(MemoryHeader* ptr);
MemoryHeader* Pop();
private:
SLIST_HEADER _header; // Lock-Free
int32 _allocSize = 0;
atomic<int32> _allocCount = 0;
USE_LOCK;
//queue<MemoryHeader*> _queue;
};
메모리 풀이란 일정크기의 메모리를 저장해뒀다가 필요할 때 꺼내쓰는 방법이다. new delete등의 kernel 연산의 overhead를 막을 수 있고, 메모리 단편화 문제를 어느정도 해결할 수 있다.
MemoryPool.cpp
#include "pch.h"
#include "MemoryPool.h"
MemoryPool::MemoryPool(int32 allocSize)
{
_allocSize = allocSize;
::InitializeSListHead(&_header); // Lock-Free
}
MemoryPool::~MemoryPool()
{
//while (_queue.empty() == false) {
// MemoryHeader* header = _queue.front(); // lock
// _queue.pop();
// ::free(header);
//}
while (MemoryHeader* memory = static_cast<MemoryHeader*>(::InterlockedPopEntrySList(&_header))) {
::_aligned_free(memory);
} // Lock-Free
}
void MemoryPool::Push(MemoryHeader* ptr)
{
//WRITE_LOCK; // Lock
ptr->_allocSize = 0;
//_queue.push(ptr); // Lock
::InterlockedPushEntrySList(&_header, static_cast<PSLIST_ENTRY>(ptr)); // lock-Free
_allocCount.fetch_sub(1);
}
MemoryHeader* MemoryPool::Pop()
{
MemoryHeader* memory = static_cast<MemoryHeader*>(::InterlockedPopEntrySList(&_header)); // Lock-Free
/*MemoryHeader* header = nullptr;*/
//{
// WRITE_LOCK;
// if (!_queue.empty()) {
// const int32 size = _queue.size(); // lock
// header = _queue.front();
// _queue.pop();
// }
//}
//없으면 새로 만듬
if (memory == nullptr) {
//header = reinterpret_cast<MemoryHeader*>(::malloc(_allocSize)); //lock
memory = reinterpret_cast<MemoryHeader*>(::_aligned_malloc(_allocSize, SLIST_ALIGHMENT)); // Lock-Free
}
else {
ASSERT_CRASH(memory->_allocSize == 0);
}
_allocCount.fetch_add(1);
return memory;
}
위의 메모리풀을 singleton으로 총괄하는 객체가 할당기 역할을 하면 된다.
Memory.h
#pragma once
#include "Allocator.h"
// Memory
class MemoryPool;
class Memory {
enum {
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;
//index table
MemoryPool* _poolTable[MAX_ALLOC_SIZE + 1];
};
// Allocate
template<typename Type, typename... Args>
Type* xnew(Args&&... args) {
Type* memory = static_cast<Type*>(XALLOC(sizeof(Type)));
new(memory) Type(std::forward<Args>(args)...);
return memory;
}
template<typename Type>
void xdelete(Type* obj) {
obj->~Type();
XRELEASE(obj);
}
XALLOC, XRELEASE는 macro설정해놨음.
Memory.cpp
#include "pch.h"
#include "Memory.h"
#include "MemoryPool.h"
Memory::Memory()
{
int size = 0;
int idxCount = 0;
for (size = 32; size < 1024; size += 32) {
MemoryPool* ptr = new MemoryPool(size);
_pools.push_back(ptr);
while (idxCount <= size) {
_poolTable[idxCount] = ptr;
idxCount++;
}
}
for (; size < 2048; size += 128) {
MemoryPool* ptr = new MemoryPool(size);
_pools.push_back(ptr);
while (idxCount <= size) {
_poolTable[idxCount] = ptr;
idxCount++;
}
}
for (; size <= 4096; size += 256) {
MemoryPool* ptr = new MemoryPool(size);
_pools.push_back(ptr);
while (idxCount <= size) {
_poolTable[idxCount] = ptr;
idxCount++;
}
}
}
Memory::~Memory()
{
for (MemoryPool* pools : _pools) {
delete pools;
}
_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));*/ //lock
header = reinterpret_cast<MemoryHeader*>(::_aligned_malloc(allocSize, SLIST_ALIGHMENT));
}
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) {
::_aligned_free(ptr); // Lock-Free
}
else {
_poolTable[allocSize]->Push(header);
}
}
lock-free와 queue를 이용한 lock 둘 모두 구현해보았는데, lock-free에 사용된 SList는 메모리 정렬이 16바이트로 이루어져야 예상치못한 오류를 방지할 수 있다.
메모리크기를 총 4kb크기까지로 두고 그 크기를 벗어난다면 메모리 단편화문제는 크게 신경쓸 필요가 없어져서 (매우 크기때문에) 그냥 동적할당을 시행한다.
메모리 크기가 작으면 그만큼 촘촘해야 효율이 더 잘 나오기 때문에 1024전까지는 32바이트씩 늘어나게 하고 2048 까진 128바이트 4096까진 256바이트가 늘어난다.
즉 32,64,128... 1024,1152...2048,2304... 4096 까지 여러크기의 메모리가 있는 셈이다.
Memory클래스에서 위의 크기마다의 메모리풀을 저장하고 있고, 메모리풀에는 각 크기마다 필요한 만큼 생성하고 반납시 메모리풀의 container에 반환된다.
요즘 window는 메모리단편화 문제를 효율적이게 해결하고 있어서 굳이 메모리풀을 사용할 필요가 없다고 하나, 리눅스에서는 다른듯하다.
'MMOServer' 카테고리의 다른 글
SocketProgramming Basic(C++) (0) | 2025.04.01 |
---|---|
ObjectPooling(C++) (0) | 2025.03.29 |
SList(C++) 불완전 (0) | 2025.03.28 |
메모리 관련 (0) | 2025.03.27 |
Smart_Pointer(C++) (0) | 2025.03.22 |