서버/메모리 관리

6. Memory Pool #1

광란의슈가슈가룬 2024. 8. 20. 07:44

지금까지는 필요할 때 메모리를 할당하고 사용이 끝나면 해제하였다. 하지만 매번 할당하고 해제하는 것이 비효율적이라고 느낄 수도 있고 메모리 단편화에 대한 문제도 있다.(요즘은 할당기가 잘 되어 있어 단편화에 대한 문제는 크게 신경쓸 필요는 없다고 한다.) 풀이라는 것은 직역하면 수영장 뭐 이런 뜻인데 수영장에 물을 담아놓는 것처럼 메모리를 담아놓았다가 필요할 때 꺼내 쓰는 것이다. 메모리를 사용하고 해제하는 것이 아니라 그냥 풀에 스윽 던져놓는 것이다. 그래서 매번 할당하고 해제할 필요성이 없다.

 

객체단위로 풀링을 하는 오브젝트 풀링이라는 것을 들어보았을 것이다. 하지만 우리가 동적할당할 것들은 객체뿐만 아니라 벡터, 맵 등 다양한 자료구조에 대해서도 동적할당을 해야한다. 이런 상황(크기가 일정하지 않음)에서 유동적으로 사용할 수 있도록 하는 것이 우리의 목표이다. 

 

MemoryPool.h

#pragma once

//-----------------
//  MemoryHeader
//-----------------

struct MemoryHeader
{
	// [MemoryHeader][Memory]
	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;
};

MemoryPool.cpp

#include "pch.h"
#include "MemoryPool.h"

//---------------
//  MemoryPool
//---------------

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;
}

delete를 할 때 객체의 사이즈가 얼마인지 어떻게 할고 삭제를 할까? 실제로 메모리를 할당할 때 데이터 앞에 해당 객체의 정보, 메모리 주소 등 다양한 정보를 담고있는 헤더가 존재한다. 여기서는 우리가 사용할 메모리의 크기를 담고 있다.

메모리 풀에서 Push는 다 쓴 메모리를 반납하는 것이고, Pop이 메모리를 사용하기 위에 메모리 풀에서 가져오는 것이다. 반대로 생각하지 않도록 하자.

 

이제 메모리 풀을 사용하기 위한 메모리 매니저를 만들어보자.

전에 Xnew, Xdelete를 만들었던 Memory.h에 매니저를 추가해보자.

 

Memory.h

#pragma once
#include "Allocator.h"

class MemoryPool;

//-----------
//  Memory
//-----------

class Memory
{
	enum
	{
		// ~1024까지 32단위, ~2048까지 128단위, ~4096까지 256단위
		POOL_COUNT = (1024 / 32) + (1024 / 128) + (1024 / 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];
};


template<typename Type, typename... Args>
Type* Xnew(Args&&... args)
{
	Type* memory = static_cast<Type*>(Xalloc(sizeof(Type)));

	//placement new
	new(memory)Type(forward<Args>(args)...);

	return memory;
}

template<typename Type>
void Xdelete(Type* obj)
{
	obj->~Type();
	Xrelease(obj);
}

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 = 1024; size <= 2048; size += 64)
	{
		MemoryPool* pool = new MemoryPool(size);
		_pools.push_back(pool);

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

	for (size = 2048; size <= 4096; size += 128)
	{
		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);
	}
}

필요한 사이즈별로 미리 풀을 만들어 놓고 이를 매핑시켜주기 위한 테이블이 있다.

 

StompAllocator의 경우 사용하지 않는 메모리를 완전히 해제해 달라고 운영체제한테 부탁하는 것이고, 메모리 풀은 재사용을 하기 위해 남겨두는 것이므로 서로 어울리지 않아 malloc, free를 사용했다. 결국 프로그래머는 둘 중 하나를 택해야한다.

 

매니저와 같은 전역 객체는 CoreGlobal에서 관리를 하고있기 때문에 추가를 해주자

CoreGlobal.h

#pragma once

extern class ThreadManager*	GThreadManager;
extern class Memory*		GMemory;

extern class DeadLockProfiler*	GDeadLockProfiler;

CoreGlobal.cpp

#include "pch.h"
#include "CoreGlobal.h"
#include "ThreadManager.h"
#include "Memory.h"
#include "DeadLockProfiler.h"

ThreadManager*		GThreadManager = nullptr;
Memory*			GMemory = nullptr;
DeadLockProfiler*	GDeadLockProfiler = nullptr;

class CoreGlobal
{
public:
	// 매니저가 추가될 경우 객체의 생성, 소멸 순서 맞추기
	CoreGlobal()
	{
		GThreadManager = new ThreadManager();
		GMemory = new Memory();
		GDeadLockProfiler = new DeadLockProfiler();
	}
	~CoreGlobal()
	{
		delete GThreadManager;
		delete GMemory;
		delete GDeadLockProfiler;
	}
} GCoreGlobal;

 

이제 Allocator도 마저 추가를 해보자.

 

Allocator.h

//-------------------
//  PoolAllocator
//-------------------

class PoolAllocator
{
public:
	static void*	Alloc(int32 size);
	static void	Release(void* ptr);
};

Allocator.cpp

//-------------------
//  PoolAllocator
//-------------------

void* PoolAllocator::Alloc(int32 size)
{
	return GMemory->Allocate(size);
}

void PoolAllocator::Release(void* ptr)
{
	GMemory->Release(ptr);
}

이제 Alloc, Release를 할 경우 알아서 풀링을 해 줄 것이다.

 

메모리 풀의 특징은 크기가 비슷한 객체들끼리 모아서 관리한다는 것이다. 다른 클래스더라도 크기가 비슷하다면 같은 풀에서 관리된다.

 

Main.cpp

#include "pch.h"
#include "CorePch.h"
#include <Windows.h>
#include "ThreadManager.h"

#include "RefCounting.h"
#include "Memory.h"
#include "Allocator.h"

class Player
{
public:
	Player()
	{
		cout << "Player()" << endl;
	}

	Player(int32 hp) : _hp(hp)
	{
		cout << "Player(hp)" << endl;
	}

	~Player()
	{
		cout << "~Player()" << endl;
	}
private:
	int32 _hp = 150;
	int32 _mp = 100;
};

int main()
{
	for (int32 i = 0; i < 5; i++)
	{
		GThreadManager->Launch([]()
			{
				while (true)
				{
					Vector<Player> v(10);

					this_thread::sleep_for(10ms);
				}
			});
	}

	GThreadManager->Join();
}

 

출력화면

멀티스레드 환경에서 정상적으로 동작하는 것을 볼 수 있다.

'서버 > 메모리 관리' 카테고리의 다른 글

8. Object Pool  (0) 2024.08.27
7. Memory Pool #2  (0) 2024.08.23
5. STL Allocator  (0) 2024.08.16
4. Stomp Allocator  (0) 2024.08.16
3. Allocator  (0) 2024.08.11