[디자인 패턴] #15_객체 풀, Object Pooling
게임 디자인 패턴 중 "최적화 패턴"에 대해 알아보겠습니다.
"게임 프로그래밍 패턴"의 19 항목, "객체 풀"에 해당하는 내용입니다.
개념
런타임 중 객체의 할당과 해제를 반복하지 않고, 고정 크기 "풀"에 할당된 객체를 재사용함으로써 메모리 사용 성능을 개선합니다!
왜 필요할까?
1. 메모리 단편화, Memory Fragmentation
- 메모리 단편화란, 힙에 사용 가능한 공간이 여유로운 크기로 뭉쳐 있지 않고, 작게 조각나 있는 상태를 의미합니다
- 전체적으로 사용 가능한 메모리 공간이 충분함에도 불구하고, 연속해서 사용 가능한 영역은 작을 수 있습니다.
- 이때, 메모리 단편화 문제 + 할당/해제 속도 -> 게임 성능을 저하시키는 요인들을 방지하기 위해 객체 풀이 필요합니다
객체 풀
1. Overview
- 재사용 가능한 객체들을 모아놓은 객체 풀 클래스를 정의합니다.
- 객체 풀에 존재하는 객체들은 각각 "사용 가능 여부"를 알려줄 방법이 필요합니다.
- 따라서, 객체 풀의 초기화는 사용할 객체들을 미리 할당받고, "사용 가능함" 상태로 초기화합니다.
- 객체가 필요할 때, 객체 풀에 요청하여 "사용 가능함" 상태의 객체를 반환 받고, 해당 객체를 "사용 불가능"상태로 변경합니다.
- 물론, 해당 객체를 모두 사용하고, 필요 없어지면 "사용 가능함"상태로 돌려놓습니다!
2. 언제 사용할까?
- 특정 객체를 높은 빈도로 생성/삭제해야 할 때!
- 객체들의 크기가 비슷할 때
- 객체를 힙에 생성할 때 성능 저하 혹은 메모리 단편화가 우려될 때
3. 사용 시 주의할 점
- 객체 풀에서 사용되지 않는 객체는 메모리 낭비입니다!
- 한 번에 사용 가능한 객체 개수가 고정되어 있습니다!
- 풀에 들어가는 객체들의 자료형이 다르다면, 크기가 가장 큰 자료형에 맞춰야 합니다!
- 재사용되는 객체는 이전 상태의 값들이 들어있으며, 객체 재사용 시 객체의 완전한 초기화가 필수적입니다.
- 사용 중이지 않은, 즉 "사용 가능함"상태의 객체도 메모리 공간을 차지하고 있습니다!
예제 코드
1. Overview
게임 월드 내에서 활용될 이펙트(Particle System) 클래스들은 생성/삭제가 빈번합니다. 파티클 클래스를 예제로 활용하여 "객체 풀" 패턴에 대해서 알아보겠습니다.
2. Particle 클래스
class Particle
{
public:
Particle() : framesLeft_(0){}
// 1. 초기화 : LifeTime을 할당받습니다.
void Init(int lifeTime) { framesLeft_ = lifeTime; }
// 2. 애니메이션 렌더링 함수
void Animate()
{
if(IsUsing() != true)
return;
--framesLeft_;
}
// 3. 사용 가능 여부 : LifeTime을 할당받고, 사용 중이면 false를 반환합니다.
bool IsUsing() const { return framesLeft_ > 0; }
private:
// 객체의 "사용 가능함" 상태 값
int framesLeft_;
};
2. ParticlePool.h (Object Pool 클래스 선언)
class ParticlePool
{
public:
// 1. "사용 가능함" 상태의 객체를 초기화합니다.
void Create(int lifeTime)
{
for (int i = 0; i < POOL_SIZE; i++)
{
if (particles_[i].IsUsing() != true)
{
particles_[i].Init(lifeTime);
break;
}
}
}
// 2. 매 프레임마다 호출되는 애니메이션 렌더링 함수
void Animate()
{
for (auto particle_ : particles_)
particle_.Animate();
}
private:
// 3. 고정 크기의 객체 풀 사이즈
static const int POOL_SIZE = 100;
// 4. 객체 풀
Particle particles_[POOL_SIZE];
};
문제 해결
1. 위 코드의 문제점
- 위 코드의 경우, "사용 가능함" 상태의 파티클 객체를 찾을 때까지 객체 풀을 순회합니다.
- 객체 풀의 크기가 커질수록, 탐색 성능에 필요한 비용이 커지는 문제가 발생합니다.
- 이때, 우리는 "빈칸 리스트"를 활용하여 위 문제를 해결할 수 있습니다.
2. Particle.h
class Particle
{
public:
Particle() : framesLeft_(0){}
void Init(int lifeTime) { framesLeft_ = lifeTime; }
// Animate 함수 수정 : 수명이 끝난 파티클 객체를 빈칸 리스트에 돌려주기위해 Life Time을 체크하도록 수정합니다.
bool Animate()
{
--framesLeft_;
return framesLeft_ == 0;
}
bool IsUsing() const { return framesLeft_ > 0; }
// 추가된 코드
// 1. 연결 리스트의 다음 Particle 객체를 가리키는 포인터 반환
Particle* GetNext() const { return state_.Next_; }
// 2. 연결 리스트의 다음 Particle 객체를 가리키는 포인터 할당
void SetNext(Particle* Next) { state_.Next_ = Next; }
private:
int framesLeft_;
// 추가된 공용체 데이터 멤버
union {
// 1. 객체가 "사용 불가능" 상태일 때, 활용되는 구조체
struct {
//...
} Live;
// 2. 객체가 "사용 가능함" 상태일 때, 활용되는 파티클 객체 포인터
Particle* Next_;
}state_;
};
2. ParticlePool.h
class ParticlePool
{
public:
ParticlePool()
{
// 1. 연결 리스트의 첫 번째 항목의 주소 값
Head = &particles_[0];
// 2. 다음 파티클 객체와 포인터를통해 연결
for (int i = 0; i < POOL_SIZE; i++)
{
particles_[i].SetNext(&particles_[i + 1]);
}
// 3. 마지막 파티클 객체의 Next는 nullptr 입니다.
particles_[POOL_SIZE - 1].SetNext(nullptr);
}
public:
// Create 함수 수정 : 새로운 파티클 객체를 빈칸 리스트에 삽입하는 작업으로 변경됩니다.
void Create(int lifeTime)
{
// 연결 리스트가 비었는지 체크
assert(Head != nullptr);
// 연결 리스트의 삽입 작업
Particle* newParticle = Head;
Head = newParticle->GetNext();
newParticle->Init(lifeTime);
}
// Animate 함수 수정 : 수명이 끝난 파티클 객체를 빈칸 리스트의 Head 앞으로 추가합니다.
void Animate()
{
for (int i = 0; i < POOL_SIZE; i++)
{
if (particles_[i].Animate() == true)
{
particles_[i].SetNext(Head);
Head = &particles_[i];
}
}
}
private:
static const int POOL_SIZE = 100;
Particle particles_[POOL_SIZE];
// 추가된 연결 리스트의 Head
Particle* Head;
};
고려 사항
1. 객체 < -> 풀 커플링
- 커플링 되면, 더 간단하게 구현할 수 있습니다.
- 커플링 되면, 객체가 풀을 통해서만 생성할 수 있도록 강제할 수 있습니다.
- 커플링 되지 않으면, 다양한 객체를 풀에 담을 수 있습니다.
- 커플링 되지 않으면, 객체의 "사용 가능함"상태를 외부에서 관리해야 합니다.
2. 재사용되는 객체의 초기화
- 풀 안에서 초기화한다면, 풀은 객체를 완전히 캡슐화할 수 있습니다.
- 풀 안에서 초기화한다면, 풀 클래스 자체는 객체가 초기화되는 방법과 결합됩니다.
- 풀 밖에서 초기화한다면, 풀의 인터페이스는 간단해집니다.
- 풀 밖에서 초기화된다면, 객체 생성의 성공 여부를 외부에서 관리해야 합니다.
'언어 > 디자인 패턴' 카테고리의 다른 글
[디자인 패턴]#16_공간 분할 패턴, Spatial Partition (0) | 2022.11.10 |
---|---|
[디자인 패턴]#14_서비스 중개자 패턴, Service Locator (0) | 2022.10.09 |
[디자인 패턴]#13_이벤트 큐, Event Queue (0) | 2022.10.02 |
[디자인 패턴]#12_Component Pattern, 컴포넌트 패턴 (0) | 2022.09.25 |
[디자인 패턴]#11_타입 객체, Type Object (0) | 2022.09.19 |