TIL - 15 (<chrono>헤더, 메모리 풀 구현 , boost라이브러리 메모리 풀 )

jh Seo·2024년 8월 20일

성능 측정(시간)

#include <chrono>

이런 식으로 chrono헤더를 포함시키고,

std::chrono::steady_clock::time_point Start{ std::chrono::steady_clock::now() };

원하는 시점에서 chrono::steady_clock::now()를 이용해 현재 timepoint를 측정한다.

auto End{ std::chrono::steady_clock::now() };

이런 식으로 원하는 시점에 또 한번 timepoint를 측정해서

auto Diff{ End - Start };

이런식으로 두 timepoint의 차를 통해 시간을 측정한다.

double DiffCount = std::chrono::duration<double, std::milli>(Diff).count();

시간차는 이런식으로 std::milli를 통해 ms로 나타낼 수 있다.

간단한 메모리 풀을 구현 후,
malloc/free , new/ delete연산과 얼마나 차이나는지 측정하는데 사용했다.

메모리풀

class FMemoryPool
{
public:
	FMemoryPool(const size_t InChunkSize, const size_t InChunkCount) noexcept
		: ChunkSize(InChunkSize)
		, ChunkCount(InChunkCount)
	{
		const size_t Align = 8;
		
		const size_t AlignedChunkSize = ((InChunkSize + (Align - 1)) & ~(Align - 1));
		const size_t TotalMemorySize = AlignedChunkSize * ChunkCount;
		StartAddress = ::_aligned_malloc(TotalMemorySize, Align);

		ActiveMemoryBlock.reserve(InChunkCount);
		for (size_t i = 0; i < ChunkCount; ++i)
		{
			uint8_t* Memory = (uint8_t*)StartAddress + i * AlignedChunkSize;
			ActiveMemoryBlock.emplace_back(Memory);
		}
	}
	FMemoryPool(const FMemoryPool&) = delete;
	FMemoryPool& operator=(const FMemoryPool&) = delete;

	~FMemoryPool()
	{
		_ASSERT(StartAddress);
		::_aligned_free(StartAddress);
	}

	void* malloc()
	{
		if (ActiveMemoryBlock.empty())
		{
			// 남은 Memory가 pool에 없다
			_ASSERT(false);
			return nullptr;
		}

		void* Memory = ActiveMemoryBlock.back();
		ActiveMemoryBlock.pop_back();
		return Memory;
	}
	
	void free(void* InMemory)
	{
		ActiveMemoryBlock.emplace_back(InMemory);
	}

private:
	const size_t ChunkSize;
	const size_t ChunkCount;
	void* StartAddress = nullptr;
	std::vector<void*> ActiveMemoryBlock;
};

구현한 메모리풀은 void형 포인터를 담은 벡터다.

간단히 설명하면 원하는 크기만큼 메모리를 미리 할당해서 void* 형으로 가지고 있다가
void*형으로 반환하는데 해당 포인터를 타입캐스팅해서 사용하면 된다.

생성자 부분

생성자에서 들어온 chunksize를 Align을 통해 메모리를 할당하는 크기를 맞춘다.
지금 코드처럼 8로 설정했다면 8의 배수로 맞춰지게끔 메모리를 할당한다.

const size_t AlignedChunkSize = ((InChunkSize + (Align - 1)) & ~(Align - 1));

이 부분은 아래 스택오버플로우글에 정리 되어있는 코드이다.
https://stackoverflow.com/questions/45213511/formula-for-memory-alignment
해당 InChunkSize에 Align-1 더해주고 align-1의 비트들을 &~ 연산을 통해 없앤다.

ex)
InChunkSize가 11이고, Align이 8이라면
(InChunkSize + (Align - 1) = 18
Align-1 = 7 = 0000 0111
~(Align-1) = 1111 1000
여기에 &연산을 하게되면 2^3 밑의 비트들은 다 0이 되고 그 위는 그대로 유지된다.
18 = 0001 0010
&연산시 0001 0000 = 16

이런식으로 청크의 크기를 설정한 align의 배수로 맞춰준 후 alignedchunksize에 저장한다.
totalmemorysize는 alignedchunksize * chunkcount가 되고,
start address에 _aligned_malloc을 통해 totalmemorysize와 align인자를 넣어
할당된 메모리블록의 시작 주소를 할당해준다.

ActiveMemoryBlock.reserve(InChunkCount);

reserve를 통해 capacity를 InChunkCount로 설정해준다.
그리고 반복문을 통해 ChunkCount갯수만큼

for (size_t i = 0; i < ChunkCount; ++i)
{
	uint8_t* Memory = (uint8_t*)StartAddress + i * AlignedChunkSize;
	ActiveMemoryBlock.emplace_back(Memory);
}

ActiveMemoryBlock에 emplace_back함수를 통해 각 청크의 시작 주소를 넣어준다.

	FMemoryPool(const FMemoryPool&) = delete;
	FMemoryPool& operator=(const FMemoryPool&) = delete;

복사생성자와 대입연산자는 delete연산자를 통해 막아둔다.

custom malloc함수

void* malloc()
{
	if (ActiveMemoryBlock.empty())
	{
		// 남은 Memory가 pool에 없다
		_ASSERT(false);
		return nullptr;
	}

	void* Memory = ActiveMemoryBlock.back();
	ActiveMemoryBlock.pop_back();
	return Memory;
}

따로 구현한 malloc에서는 만약 미리할당한 ActiveMemoryBlock이 empty상태라면
남은 메모리가 없을 때 할당 요청이 들어온 것이므로 nullptr을 반환한다.

empty상태가 아니라면 ActiveMemoryBlock의 마지막 포인터 원소를 전달하고
해당 메모리를 pop_back 시켜준다.

custom free함수

	void free(void* InMemory)
	{
		ActiveMemoryBlock.emplace_back(InMemory);
	}

free는 간단하다.
free는 사용하던 메모리를 해제하는 과정이므로
메모리풀 입장에선 해당 메모리를 다시 벡터에 넣어주면 된다.

FClass malloc/free로 시간 측정

class FClass
{
public:
	FClass()
	{
		//std::cout << __FUNCTION__ << std::endl;
	}
	FClass(int InValue)
		: Data2(InValue)
	{

	}
	~FClass()
	{
		//std::cout << __FUNCTION__ << std::endl;
	}

private:
	char Data[1024] = {};
	int Data2 = {};
};

이렇게 생긴 간단한 FClass 클래스를 100만번 malloc/free했을 때, 시간 측정을 해 비교해보기로 했다.

FClass** Arr = new FClass * [MaxCount];
std::chrono::steady_clock::time_point Start{ std::chrono::steady_clock::now() };

{
	for (size_t i = 0; i < MaxCount; ++i)
	{
		FClass* Test = (FClass*)malloc(sizeof(FClass));
		Arr[i] = Test;
	}

	for (size_t i = 0; i < MaxCount; ++i)
	{
		free(Arr[i]);
	}
}
auto End{ std::chrono::steady_clock::now() };
auto Diff{ End - Start };

delete[] Arr;
double DiffCount = std::chrono::duration<double, std::milli>(Diff).count();
#if _DEBUG
		std::cout << std::format("Debug: ");
#else
		std::cout << std::format("Release: ");
#endif
		std::cout << std::format("[FClass] malloc, free: {}ms\n", DiffCount);

이정도 시간이 나온다.

FClass new/delete로 시간 측정

FClass** Arr = new FClass * [MaxCount];
std::chrono::steady_clock::time_point Start{ std::chrono::steady_clock::now() };

{
	for (size_t i = 0; i < MaxCount; ++i)
	{
		FClass* Test = new FClass();
		Arr[i] = Test;
	}

	for (size_t i = 0; i < MaxCount; ++i)
	{
		delete Arr[i];
	}
}
auto End{ std::chrono::steady_clock::now() };
auto Diff{ End - Start };

delete[] Arr;
double DiffCount = std::chrono::duration<double, std::milli>(Diff).count();
#if _DEBUG
	std::cout << std::format("Debug: ");
#else
	std::cout << std::format("Release: ");
#endif
	std::cout << std::format("[FClass] new, delete: {}ms\n", DiffCount);

malloc, free보다는 좀 더 느리다.

FClass 구현한 메모리풀로 측정

FClass** Arr = new FClass * [MaxCount];
FMemoryPool MemoryPool = FMemoryPool(sizeof(FClass), MaxCount);

std::chrono::steady_clock::time_point Start{ std::chrono::steady_clock::now() };
{
	for (size_t i = 0; i < MaxCount; ++i)
	{
		FClass* Test = (FClass*)MemoryPool.malloc();
		Arr[i] = Test;
	}

	for (size_t i = 0; i < MaxCount; ++i)
	{
		MemoryPool.free(Arr[i]);
	}
}
auto End{ std::chrono::steady_clock::now() };
auto Diff{ End - Start };

delete[] Arr;

double DiffCount = std::chrono::duration<double, std::milli>(Diff).count();
#if _DEBUG
		std::cout << std::format("Debug: ");
#else
		std::cout << std::format("Release: ");
#endif
		std::cout << std::format("[FClass] Custom MemoryPool: {}ms\n", DiffCount);

boost 라이브러리 메모리풀 pool

FClass** Arr = new FClass * [MaxCount];

boost::pool<> MemoryPool(sizeof(FClass), MaxCount);
int* TryAlloc = (int*)MemoryPool.malloc();
MemoryPool.free(TryAlloc);

std::chrono::steady_clock::time_point Start{ std::chrono::steady_clock::now() };
{
	for (size_t i = 0; i < MaxCount; ++i)
	{
		FClass* Test = (FClass*)MemoryPool.malloc();
		Arr[i] = Test;
	}

	for (size_t i = 0; i < MaxCount; ++i)
	{
		MemoryPool.free(Arr[i]);
	}
}
auto End{ std::chrono::steady_clock::now() };
auto Diff{ End - Start };

delete[] Arr;
double DiffCount = std::chrono::duration<double, std::milli>(Diff).count();
#if _DEBUG
		std::cout << std::format("Debug: ");
#else
		std::cout << std::format("Release: ");
#endif
		std::cout << std::format("[FClass] boost MemoryPool: {}ms\n", DiffCount);

boost라이브러리를 설치한 후 , pool을 사용해봤다.

업로드중..

디버깅에서는 custom memorypool이 훨씬 느리지만
릴리즈 모드에서는 오히려 boost라이브러리의 memory pool이 느리게 나왔다.
디버그모드에서 벡터가 느리기도 하고 애초에 단순 할당 해제만 비교해서 그런것 같다.

profile
코딩 창고!

0개의 댓글