서버/메모리 관리

8. Object Pool

광란의슈가슈가룬 2024. 8. 27. 09:10

저번의 메모리 풀에는 뭔가가 아쉬운 부분이 있다. 직접 구현한 메모리 풀에서는 사이즈가 비슷한 객체들을 하나의 풀에 넣어서 관리를 했다. 사이즈가 조금이라도 다른 객체들이 같은 풀에 있다보니 해당 풀에서 메모리 오염이 발생할 가능성이 있다. 만약 어떤 한 클래스에서 오류가 났다면 그 클래스의 코드만 살펴보면 되지만 이런 경우 어디서 오류가 발생했는지 탐지하기도 굉장히 힘들다.

 

그래서 이번에 알아볼 것이 오브젝트 풀이다. 생각해보면 알겠지만 메모리 풀이 오브젝트 풀의 상위 개념이다. 오브젝트 풀을 포함하고 있다는 것이다. 

 

ObjectPool.h

#pragma once
#include "Types.h"
#include "MemoryPool.h"

template<typename Type>
class ObjectPool
{
public:
	template<typename... Args>
	static Type* Pop(Args&&...args)
	{
		Type* memory = static_cast<Type*>(MemoryHeader::AttachHeader(s_pool.Pop(), s_allocSize));
		//placement new
		new(memory)Type(forward<Args>(args)...);
		return memory;
	}

	static void Push(Type* obj)
	{
		obj->~Type();
		s_pool.Push(MemoryHeader::DetachHeader(obj));
	}

private:
	static int32		s_allocSize;
	static MemoryPool	s_pool;
};

template<typename Type>
int32 ObjectPool<Type>::s_allocSize = sizeof(Type) + sizeof(MemoryHeader);

template<typename Type>
MemoryPool ObjectPool<Type>::s_pool{ s_allocSize };

Memory.h의 Xnew와 Xdelete의 코드랑 거의 흡사하지만 위의 코드는 같은 객체를 풀링한다는 차이가 있다.

 

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:
	int32 _hp = 100;
};

class Monster
{
public:
	int32 _id = 0;
};

int main()
{
	Player* p = ObjectPool<Player>::Pop();
	Monster* m = ObjectPool<Monster>::Pop();

	ObjectPool<Player>::Push(p);
	ObjectPool<Monster>::Push(m);
}

이런식으로 사용할 수 있다.

 

하지만 오브젝트 풀을 이용해서 할당을 했는데 해제할 때 실수로 delete나 Xdelete와 같이 해제를 하면 어떡하나 하는 생각이 들 수 있다. 그래서 shared_ptr를 이용해 해제할 때 자동으로 오브젝트 풀의 소멸자를 이용하도록 유도할 것이다. 

 

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

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

class Player
{
public:
	int32 _hp = 100;
};

class Monster
{
public:
	int32 _id = 0;
};

int main()
{
	//shared_ptr<Player> p = make_shared<Player>();
	shared_ptr<Player> sptr = { ObjectPool<Player>::Pop() , ObjectPool<Player>::Push };
}

전에 알아본 shared_ptr의 사용방법은 주석으로 처리된 방법일 것이다. 하지만 다른 방법으로 이렇게 할당자와 소멸자를 건내주는 방법이 있다. 이렇게 하면 객체를 사용하고 나면 알아서 오브젝트 풀로 돌아갈 것이다.

 

그런데 저렇게 타이핑하는 것 마저 귀찮다고 하면 그냥 함수를 하나 더 만들면 된다.

 

ObjectPool.h

#pragma once
#include "Types.h"
#include "MemoryPool.h"

template<typename Type>
class ObjectPool
{
public:
	template<typename... Args>
	static Type* Pop(Args&&...args)
	{
		Type* memory = static_cast<Type*>(MemoryHeader::AttachHeader(s_pool.Pop(), s_allocSize));
		//placement new
		new(memory)Type(forward<Args>(args)...);
		return memory;
	}

	static void Push(Type* obj)
	{
		obj->~Type();
		s_pool.Push(MemoryHeader::DetachHeader(obj));
	}

	static shared_ptr<Type> MakeShared()
	{
		shared_ptr<Type> ptr = { Pop(), Push };

		return ptr;
	}

private:
	static int32		s_allocSize;
	static MemoryPool	s_pool;
};

template<typename Type>
int32 ObjectPool<Type>::s_allocSize = sizeof(Type) + sizeof(MemoryHeader);

template<typename Type>
MemoryPool ObjectPool<Type>::s_pool{ s_allocSize };

 

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:
	int32 _hp = 100;
};

class Monster
{
public:
	int32 _id = 0;
};

int main()
{
	shared_ptr<Player> sptr = ObjectPool<Player>::MakeShared();
}

MakeShared라는 함수를 만들어서 이렇게 할당한 후 사용하면 되겠다.

 

오브젝트 풀에 대해서는 이정도가 끝이다. 하지만 PoolAllocator를 사용하게 되면 StompAllocator를 사용할 수 없는데 이는 프로그래머의 선택이다. 하지만 서버는 처음에 오류를 잡는 것이 중요하기 때문에 풀을 사용할 때 오류를 검출할 수 있도록 코드를 수정해보자.

 

ObjectPool.h

#pragma once
#include "Types.h"
#include "MemoryPool.h"

template<typename Type>
class ObjectPool
{
public:
	template<typename... Args>
	static Type* Pop(Args&&...args)
	{
#ifdef _STOMP
		MemoryHeader* ptr = reinterpret_cast<MemoryHeader*>(StompAllocator::Alloc(s_allocSize));
		Type* memory = static_cast<Type*>(MemoryHeader::AttachHeader(ptr, s_allocSize));
#else
		Type* memory = static_cast<Type*>(MemoryHeader::AttachHeader(s_pool.Pop(), s_allocSize));
#endif
		//placement new
		new(memory)Type(forward<Args>(args)...);
		return memory;
	}

	static void Push(Type* obj)
	{
		obj->~Type();

#ifdef _STOMP
		StompAllocator::Release(MemoryHeader::DetachHeader(obj));
#else
		s_pool.Push(MemoryHeader::DetachHeader(obj));
#endif
	}

	static shared_ptr<Type> MakeShared()
	{
		shared_ptr<Type> ptr = { Pop(), Push };

		return ptr;
	}

private:
	static int32		s_allocSize;
	static MemoryPool	s_pool;
};

template<typename Type>
int32 ObjectPool<Type>::s_allocSize = sizeof(Type) + sizeof(MemoryHeader);

template<typename Type>
MemoryPool ObjectPool<Type>::s_pool{ s_allocSize };

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

#ifdef _STOMP
	header = reinterpret_cast<MemoryHeader*>(StompAllocator::Alloc(allocSize));
#else
	if (allocSize > MAX_ALLOC_SIZE)
	{
		// 풀링 최대 크기를 벗어나면 일반 할당
		header = reinterpret_cast<MemoryHeader*>(::_aligned_malloc(allocSize, SLIST_ALIGNMENT));
	}
	else
	{
		// 풀에서 꺼내옴
		header = _poolTable[allocSize]->Pop();
	}
#endif

	return MemoryHeader::AttachHeader(header, allocSize);
}

void Memory::Release(void* ptr)
{
	MemoryHeader* header = MemoryHeader::DetachHeader(ptr);

	const int32 allocSize = header->allocSize;
	ASSERT_CRASH(allocSize > 0);

#ifdef _STOMP
	StompAllocator::Release(header);
#else
	if (allocSize > MAX_ALLOC_SIZE)
	{
		// 풀링 최대 크기를 벗어나면 일반 해제
		::_aligned_free(header);
	}
	else
	{
		// 풀에 메모리 반납
		_poolTable[allocSize]->Push(header);
	}
#endif
}

이렇게 해주면 _STOMP가 정의되어 있을 때 StompAllocator를 사용하고 아니면 오브젝트 풀을 사용할 것이다.

#define _STOMP

 

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*>(PoolAllocator::Alloc(sizeof(Type)));

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

	return memory;
}

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

template<typename Type>
shared_ptr<Type> MakeShared()
{
	return shared_ptr<Type>{Xnew<Type>(), Xdelete<Type>};
}

MakeShared라는 함수도 추가하여 오브젝트 풀와 메모리 풀 중 골라서 사용할 수 있도록 하자.

 

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:
	int32 _hp = 100;
};

int main()
{
	shared_ptr<Player> sptr = ObjectPool<Player>::MakeShared(); 	// 오브젝트 풀
	shared_ptr<Player> sptr2 = MakeShared<Player>();		// 메모리 풀
}

 

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

7. Memory Pool #2  (0) 2024.08.23
6. Memory Pool #1  (0) 2024.08.20
5. STL Allocator  (0) 2024.08.16
4. Stomp Allocator  (0) 2024.08.16
3. Allocator  (0) 2024.08.11