MMOServer

MemoryPool(C++)

이야기prog 2025. 3. 28. 22:33

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