상세 컨텐츠

본문 제목

Memory pool

똑똑한 개발/C++ 게임개발

by 성댕쓰 2021. 9. 15. 22:27

본문

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)

'똑똑한 개발 > C++ 게임개발' 카테고리의 다른 글

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

관련글 더보기

댓글 영역