메모리 풀에 이어서 이번에는 오브젝트 풀을 스스로 구현해보았다. 사실 구현이라고 할 만큼 뭔가 대단한 걸 한건 아니고 그냥 내가 이해한 걸 간략하게나마 코드로 짜본 것인데 뒤에 언급하겠지만 사실 성능상의 문제점도 존재하고 완벽히 정확하게 구현했는지는 잘 모르겠다.
우선 오브젝트 풀이란 프로그램 내에서 사용할 오브젝트들을 container(ex. list, vector)나 container adaptor(ex. queue, stack)에 담아 두었다가 사용자가 요청시 꺼내서 주고, 반환시 container에 다시 담아두는 것을 의미한다. 만약 동적 할당및 해제가 잦은 프로그램내에서 오브젝트 풀을 사용한다면 잦은 할당및 해제로 인한 오버헤드와 메모리 단편화도 조금 방지할 수 있다고 생각한다(사실 요즘 memory allocator는 메모리 단편화가 거의 없다고 한다).
핵심 로직은 CObjectPoolManager 클래스에 있는 생성자, Allocate, Deallocate함수이다.
우선 CObjectPoolManager의 생성자에서는 initObjectCount만큼 Object를 메모리 할당해서 m_ObjectVector에 넣어둔다. 그리고 Allocate가 호출 될 때 마다 m_ObjectVector에서 꺼내서 리턴해주고 Deallocate가 호출되면 다시 m_ObjectVector에 넣는다.
오브젝트를 담아두는 container로 나는 벡터를 사용했다. 구현한 다른 분들의 코드를 보면 queue, list로도 구현한 걸 볼 수 있었는데 나는 실제로 다음과 같은 코드로 pre_vector(reserve로 미리 공간을 할당해둔 뒤에 push_back), vector, list, queue, stack을 시험해보았다.
#define LOOP_COUNT 10000000
#include <iostream>
#include <vector>
#include <stack>
#include <list>
#include <queue>
#include <chrono>
int main()
{
std::vector<int> MyVec;
std::stack<int> MyStack;
std::list<int> MyList;
std::queue<int> MyQueue;
auto start = std::chrono::high_resolution_clock::now();
MyVec.reserve(10);
for (int i = 0; i < LOOP_COUNT; ++i)
{
for (int j = 0; j < 10; ++j)
{
MyVec.emplace_back(3);
}
for (int j = 0; j < 10; ++j)
{
MyVec.pop_back();
}
}
auto end = std::chrono::high_resolution_clock::now();
std::chrono::seconds diff = std::chrono::duration_cast<std::chrono::seconds>(end - start);
std::cout << "vector: " << diff.count() << std::endl;
// =====================================================================
start = std::chrono::high_resolution_clock::now();
for (int i = 0; i < LOOP_COUNT; ++i)
{
for (int j = 0; j < 10; ++j)
{
MyStack.emplace(3);
}
for (int j = 0; j < 10; ++j)
{
MyStack.pop();
}
}
end = std::chrono::high_resolution_clock::now();
diff = std::chrono::duration_cast<std::chrono::seconds>(end - start);
std::cout << "stack: " << diff.count() << std::endl;
// =====================================================================
start = std::chrono::high_resolution_clock::now();
for (int i = 0; i < LOOP_COUNT; ++i)
{
for (int j = 0; j < 10; ++j)
{
MyQueue.emplace(3);
}
for (int j = 0; j < 10; ++j)
{
MyQueue.pop();
}
}
end = std::chrono::high_resolution_clock::now();
diff = std::chrono::duration_cast<std::chrono::seconds>(end - start);
std::cout << "queue: " << diff.count() << std::endl;
// =====================================================================
start = std::chrono::high_resolution_clock::now();
for (int i = 0; i < LOOP_COUNT; ++i)
{
for (int j = 0; j < 10; ++j)
{
MyList.emplace_back(3);
}
for (int j = 0; j < 10; ++j)
{
MyList.pop_back();
}
}
end = std::chrono::high_resolution_clock::now();
diff = std::chrono::duration_cast<std::chrono::seconds>(end - start);
std::cout << "list: " << diff.count() << std::endl;
return 0;
}
어차피 프로젝트에서도 포인터를 넣어주므로 container 입장에서는 정수값을 넣어주는 것이랑 다를바 없다 생각해서 정수를 담는 container들로 실험해봤다. 위와 같은 간단한 방법으로 실험해보니 아래와 같이 vector가 가장 빨랐다.
특히 vector에 넣어줄 element의 갯수를 미리 알고 있다면 reserve함수를 이용해서 미리 공간을 할당해놓고 push_back이나 emplace_back을 하는 것이 더 빠르기 때문에 생성자에서 reserve함수를 사용했다.
CObjectPoolManager::CObjectPoolManager(int count) : m_InitObjectCount(count)
{
// 생성자 호출될 때 인자만큼 stack에 미리 오브젝트 공간 reserve
m_ObjectVector.reserve(m_InitObjectCount);
for (int i = 0; i < m_InitObjectCount; ++i)
{
m_ObjectVector.emplace_back(new CObject);
}
}
Performance
Limitation
내가 구현한 오브젝트 풀의 한계점중 오브젝트 풀을 사용하지 않은 것과 사용한 것이 성능 차이가 거의 없다는 점이다. 루프를 한번 돌때마다 20개의 오브젝트들을 할당받았다가 해제하는 상황에서 성능측정을 해보았다. 좀 더 최적화 할 수 있는 방법을 연구해보는게 다음 목표다.
Git
https://github.com/DkLee3/Pool-Project
Reference
https://github.com/SnowFleur/2020-Pool-Patterns
'공부 > 그 외' 카테고리의 다른 글
C7510 : 종속적 형식 이름은 'typename' 접두사와 함께 사용해야 합니다. (1) | 2021.08.12 |
---|---|
꼬리 재귀(Tail recursion) (0) | 2021.08.04 |
Visual Studio "const char *" 형식의 값을 사용하여 "char *" 형식의 엔터티를 초기화할 수 없습니다. (0) | 2021.07.16 |
메모리 풀(Memory Pool) (0) | 2021.07.14 |
Handle, Handler란? (0) | 2021.07.09 |