학부생때 들어보기만한 메모리풀을 직접 구현해봤다. 듣기만 해서 대충 개념만 알고 있어서 직접 구현하려니 어디서부터 시작해야할지 몰랐다. 그래서 구체적으로 메모리 풀이 어떤건지 개념을 다시 정리하는 것 부터 시작했다. 위키피디아에선 메모리 풀은 고정된 크기의 블록을 할당하여 malloc이나 C++의 new 연산자와 유사한 메모리 동적 할당을 가능하게 해준다. memory pool은 불리는 동일한 사이즈의 메모리 블록들을 미리 할당해 놓는다. 그러면 응용 프로그램들은 실행 시간에 핸들에 의해서 표현되는 블록들을 할당하고, 접근하고, 해제할 수 있다.
이 말을 조금 나의 방식대로 풀어서 설명하면, 거대한 덩어리의 메모리 블록을 미리 할당해놓고, 이것을 사용자가 원하는 만큼 조금씩 떼어서 준다는 것이다.
왜 사용해야 하는가?
malloc, new에는 자체적으로 메모리 단편화 방지 알고리즘이 포함되어 있지만 고가용성의 (고가용성(High Availability): 서버와 네트워크, 프로그램 등의 정보 시스템이 상당히 오랜 기간 동안 지속적으로 정상 운영이 가능한 성질을 말한다) 메모리 파편화(fragmentation)가 생길 수도 있다. 그리고 malloc이나 new도 내부에서 system call로 인한 overhead가 생길 수 있다. stackoverflow의 답변을 보자
해석하면 malloc과 new 자체로는 system call이 아니지만, 메모리 할당을 하기 위해 low-level mechanism을 사용하고 윈도우에서는 VirtualAlloc() 함수를, POSIX에서는 mmap()이라는 system call을 사용한다고 한다. 그리고 연속된 메모리 할당이라면 system call없이 할당을 해줄 수도 있다고 한다. 결국 결론은 malloc, new로 인한 오버헤드가 있을 수 있다는 의미로 보인다.
코드 리뷰
CTest::RunPool() 함수에서 아래와 같이 PoolManager에게 메모리 할당을 요청한다. PoolManager는 CTest 클래스에 이렇게 선언되어 있다
std::unique_ptr<CPoolManager> PoolManager
unique_ptr은 이 객체가 가리키는 주소를 다른 포인터도 가르키고 있지 않게 하기위해서 + 이전에 공부해본거라 한번 써먹어 보고 싶기도 해서 써봤다.
void CTest::RunPool()
{
for (int i = 0; i < TEST_LOOP_COUNT; ++i)
{
int* ptr1 = reinterpret_cast<int*>(PoolManager->Allocate(100));
PoolManager->Deallocate(ptr1);
}
}
RunPool함수에서 void* CPoolManager::Allocate(size_t size)로 점프하는데 만약 메모리 할당이 처음이라면 CMemoryBlock* 객체인 MemoryBlockHandle을 이용해서 void* CMemoryBlock::Allocate(size_t size)를 호출한다. CMemoryBlock::Allocate에서는 메모리를 size단위로 조각내서 linked list 방식으로 연결해준다. CMemoryBlock::Allocate의 리턴값은 CMemoryBlock의 생성자에서 malloc을 사용해서 arena_size만큼 할당한 거대한 메모리 덩어리(Allocate 해줄 때 마다 반환해줄 각 메모리 블록의 모음)의 시작 주소이다.
int count = GetCount();
auto ret = m_FreeBlock;
for (int i = 0; i < count; ++i)
{
auto p = reinterpret_cast<char*>(m_FreeBlock) + size;
if (i != count - 1)
{
m_FreeBlock->setNext(p);
}
else
{
m_FreeBlock->setNext(nullptr);
}
m_FreeBlock = m_FreeBlock->GetNext();
}
만약 메모리 할당이 처음이 아니라면 CMemoeyBlock::Allocate를 호출하지 않고 CPoolManager::Allocate에서 단순히 다음 블록을 리턴해준다.
auto ret = m_MemoryChunk;
m_MemoryChunk = m_MemoryChunk->GetNext();
return ret;
Performance 비교
malloc/free를 사용할 때 걸린 시간과 내가 사용한 Allocate/Deallocate를 사용할 때 걸린 시간을 비교하는 방식으로 performance를 비교해보았다. 당연히 단순히 malloc을 사용한 메모리 할당보단 좋을 것이라 생각했지만 생각보다 성능이 드라마틱하게 좋아지진 않았던 것 같다.
Limitation
내가 참고한 서적이나 블로그의 코드들보다 성능이 떨어졌다. 어떻게 하면 개선시킬 수 있는지 고민해볼 것이다.
구현한 코드는 https://github.com/DkLee3/Pool-Project 여기에 올려두었다.
Reference
https://ko.wikipedia.org/wiki/%EB%A9%94%EB%AA%A8%EB%A6%AC_%ED%92%80
https://snowfleur.tistory.com/171
https://www.risebong.co.kr/%EB%A9%94%EB%AA%A8%EB%A6%AC-%EB%8B%A8%ED%8E%B8%ED%99%94/
https://ko.wikipedia.org/wiki/%EA%B3%A0%EA%B0%80%EC%9A%A9%EC%84%B1
'공부 > 그 외' 카테고리의 다른 글
꼬리 재귀(Tail recursion) (0) | 2021.08.04 |
---|---|
오브젝트 풀(Object Pool) (0) | 2021.07.19 |
Visual Studio "const char *" 형식의 값을 사용하여 "char *" 형식의 엔터티를 초기화할 수 없습니다. (0) | 2021.07.16 |
Handle, Handler란? (0) | 2021.07.09 |
레거시(legacy)란? (0) | 2021.07.08 |