언어/디자인 패턴

[디자인 패턴]#15_객체 풀, Object Pooling

Hardii2 2022. 11. 2. 10:55

 

[디자인 패턴] #15_객체 풀, Object Pooling

게임 디자인 패턴 중 "최적화 패턴"에 대해 알아보겠습니다.

"게임 프로그래밍 패턴"의 19 항목, "객체 풀"에 해당하는 내용입니다.

 

 


 

개념
런타임 중 객체의 할당과 해제를 반복하지 않고, 고정 크기 "풀"에 할당된 객체를 재사용함으로써 메모리 사용 성능을 개선합니다!

 

왜 필요할까?

1. 메모리 단편화, Memory Fragmentation

출저: 게임 프로그래밍 패턴 362pg

  • 메모리 단편화란, 힙에 사용 가능한 공간이 여유로운 크기로 뭉쳐 있지 않고, 작게 조각나 있는 상태를 의미합니다
  • 전체적으로 사용 가능한 메모리 공간이 충분함에도 불구하고, 연속해서 사용 가능한 영역은 작을 수 있습니다.
  • 이때, 메모리 단편화 문제 + 할당/해제 속도 -> 게임 성능을 저하시키는 요인들을 방지하기 위해 객체 풀이 필요합니다

 

객체 풀

1. Overview

  • 재사용 가능한 객체들을 모아놓은 객체 풀 클래스를 정의합니다.
  • 객체 풀에 존재하는 객체들은 각각 "사용 가능 여부"를 알려줄 방법이 필요합니다.
  • 따라서, 객체 풀의 초기화는 사용할 객체들을 미리 할당받고, "사용 가능함" 상태로 초기화합니다. 
  • 객체가 필요할 때, 객체 풀에 요청하여 "사용 가능함" 상태의 객체를 반환 받고, 해당 객체를 "사용 불가능"상태로 변경합니다.
  • 물론, 해당 객체를 모두 사용하고, 필요 없어지면 "사용 가능함"상태로 돌려놓습니다!

 

2. 언제 사용할까?

  1. 특정 객체를 높은 빈도로 생성/삭제해야 할 때!
  2. 객체들의 크기가 비슷할 때
  3. 객체를 힙에 생성할 때 성능 저하 혹은 메모리 단편화가 우려될 때

 

3. 사용 시 주의할 점

  1. 객체 풀에서 사용되지 않는 객체는 메모리 낭비입니다!
  2. 한 번에 사용 가능한 객체 개수가 고정되어 있습니다!
  3. 풀에 들어가는 객체들의 자료형이 다르다면, 크기가 가장 큰 자료형에 맞춰야 합니다!
  4. 재사용되는 객체는 이전 상태의 값들이 들어있으며, 객체 재사용 시 객체의 완전한 초기화가 필수적입니다.
  5. 사용 중이지 않은, 즉 "사용 가능함"상태의 객체도 메모리 공간을 차지하고 있습니다!

 

  예제 코드

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. 재사용되는 객체의 초기화

  • 풀 안에서 초기화한다면, 풀은 객체를 완전히 캡슐화할 수 있습니다.
  • 풀 안에서 초기화한다면, 풀 클래스 자체는 객체가 초기화되는 방법과 결합됩니다.
  • 풀 밖에서 초기화한다면, 풀의 인터페이스는 간단해집니다.
  • 풀 밖에서 초기화된다면, 객체 생성의 성공 여부를 외부에서 관리해야 합니다.