메모리 단편화를 막기 위한 방법 중 하나
결론은 메모리를 크게 한번에 잡아놓고 계속 들고 있으면서 마음껏 객체를 할당, 해제하는 것이다.
1. 먼저 재사용 가능한 객체들을 모아놓은 객체 풀 클래스를 정의한다.
2. 여기에 들어가는 객체는 현재 자신이 사용 중인지 여부를 알 수 있는 방법을 제공해야 한다.
3. 풀은 초기화할때 사용할 객체들을 미리 생성하고(보통 같은 종류의 객체를 연속된 배열에 넣는다.), 그리고 사용안함으로 초기화한다.
4. 새로운 객체가 필요하면 풀에 요청을 하고, 요청된 풀은 사용 중으로 초기화해서 반환하고..더이상 사용안하면 사용안함으로 초기화
보통 시각적 효과같이 눈으로 볼 수 있는 것에 많이 사용된다. 물론 사운드도 해당한다.
어쨌든 다음과 같을때 사용한다.
- 객체를 빈번하게 생성, 삭제를 해야한다.
- 객체들의 크기가 비슷할때
- 객체를 힙에 생성하기가 느리거나 메모리 단편화가 우려될때
주의사항
- 객체 풀에서 사용되지 않는 객체는 메모리 낭비와 다를 바 없다.
- 한번에 사용 가능한 객체 개수가 정해져 있다.
(객체 풀의 모든 객체가 사용 중이어서 재사용할 객체를 반환받지 못할 때를 대비해야 한다)
- 객체를 위한 메모리 크기는 고정되어 있다.
(메모리 관리자는 블록 크기에 따라 풀을 여러개 준비한다라..메모리 요청을 받으면 크기에 맞는 풀을 준다라..)
- 재사용되는 객체는 저절로 초기화 되지 않는다.
객체 풀 관련 클래스
// 객체 풀에서 사용하게 될 클래스
class Particle
{
public:
Particle() : framesLeft(0) {}
void init(double x, double y, double xVel, double yVel, int lifetime);
void animate();
bool inUse() const { return framesLeft > 0; }
private:
int framesLeft;
double x_, y_;
double xVel_, yVel_;
};
void Particle::init(double x, double y, double xVel, double yVel, int lifetime)
{
x_ = x;
y_ = y;
xVel_ = xVel;
yVel_ = yVel;
framesLeft = lifetime;
}
void Particle::animate()
{
if (!inUse())
return;
framesLeft--;
x_ += xVel_;
y_ += yVel_;
}
//객체 풀 클래스
class ParticlePool
{
public:
void create(double x, double y, double xVel, double yVel, int lifetime);
void animate();
private:
static const int POOL_SIZE = 100;
Particle particles_[POOL_SIZE];
};
void ParticlePool::animate()
{
for (int i = 0; i < POOL_SIZE; ++i)
{
particles_[i].animate();
}
}
void ParticlePool::create(double x, double y, double xVel, double yVel, int lifetime)
{
//파티클을 생성할 떄마다 사용가능 한 객체 떄문에 전체 순회를 해야한다. 사이즈가 크다면
//충분히 느려질 이유가 된다.
for (int i = 0; i < POOL_SIZE; ++i)
{
if (!particles_[i].inUse())
{
particles_[i].init(x, y, xVel, yVel, lifetime);
return;
}
}
}
객체 풀에 사용될 클래스는 단순하다..그냥 일반 클래스임.
예를들어 파티클 클래스라면 그냥 파티클에 관련된 데이터가 있는 클래스다.
객체 풀 클래스는 위의 사용될 클래스를 배열로 가지고 있다.
사용 가능한 객체를 풀에서 찾기위해서 만약에 단순하게 구현하면..
for( i = 0; i < 전체 풀 수; ++i)
{
if(현재 풀에서 그 그객체가 사용되지않으면)
이때 그 풀을 초기화하고 리턴하는 거임..
}
이러면 전체 순회때문에 느릴 수 있다(풀의 크기가 크다면..)
그렇다 사용 가능한 파티클을 찾느라고 시간을 낭비하기 싫다면 이를 계속 추적해야 한다.
그래서 나온것이 빈칸 리스트라는데..이 개념이.. 좀 신기방기..
빈칸리스트
현재 객체 풀에서 사용하지 않은 메모리를 끌어다가 리스트 처럼 한다는데..
사용되지 않은 파티클 클래스에서의 위치나 속도 같은 데이터 대부분은 의미가 없다.(왜냐면 안쓰니까..) 오직 파티클이 죽어 있는지를
알려주는 상태 하나만 사용된다. 위의 변수만 봤을때는 framesLeft만 있으면 되고 나머지 데이터는 다른 용도로 사용할 수 있다.
파티클 클래스를 수정하게 되면..
수정된 파티클 클래스
// 객체 풀에서 사용하게 될 클래스
//class Particle
//{
//public:
// Particle() : framesLeft(0) {}
// void init(double x, double y, double xVel, double yVel, int lifetime);
// void animate();
// bool inUse() const { return framesLeft > 0; }
//
//private:
// int framesLeft;
// double x_, y_;
// double xVel_, yVel_;
//};
//
//void Particle::init(double x, double y, double xVel, double yVel, int lifetime)
//{
// x_ = x;
// y_ = y;
// xVel_ = xVel;
// yVel_ = yVel;
// framesLeft = lifetime;
//}
//
//void Particle::animate()
//{
// if (!inUse())
// return;
//
// framesLeft--;
// x_ += xVel_;
// y_ += yVel_;
//}
//빈칸 리스트를 위해 수정된 클래스
class Particle
{
public:
Particle() : framesLeft(0) {}
void init(double x, double y, double xVel, double yVel, int lifetime);
void animate();
bool inUse() const { return framesLeft > 0; }
//수정내용
Particle* getNext() const { return state_.next; }
void setNext(Particle* next)
{
state_.next = next;
}
private:
int framesLeft;
//유니온으로 정의해서 메모리 시작위치가 같게 만든다.
//여기선 구조체랑 next가 공유하는것.
union
{
//사용중일떄 사용하는 것.
struct
{
double x, y;
double xVel, yVel;
}live;
//사용중이지 않을떄 사용하는것.
//이것을 이용해서 풀에서 사용 가능한 파티클이 묶여 있는 연결 리스트를 만들 수 있다.
//이렇게 하면 추가 메모리 없이 죽어 있는 객체의 메모리를 재활용해서 자기 자신을
//사용 가능한 파티클 리스트에 등록하게 할 수 있다.
//이런걸 빈칸 리스트라고 함.
Particle* next;
}state_;
};
이렇게 되면 새로 추가된 포인터를 관리해야하는데 객체풀도 수정이 된다.
수정된 객체 풀
////객체 풀 클래스
//class ParticlePool
//{
//public:
// void create(double x, double y, double xVel, double yVel, int lifetime);
// void animate();
//
//private:
// static const int POOL_SIZE = 100;
// Particle particles_[POOL_SIZE];
//};
//
//void ParticlePool::animate()
//{
// for (int i = 0; i < POOL_SIZE; ++i)
// {
// particles_[i].animate();
// }
//}
//
//void ParticlePool::create(double x, double y, double xVel, double yVel, int lifetime)
//{
// //파티클을 생성할 떄마다 사용가능 한 객체 떄문에 전체 순회를 해야한다. 사이즈가 크다면
// //충분히 느려질 이유가 된다.
// for (int i = 0; i < POOL_SIZE; ++i)
// {
// if (!particles_[i].inUse())
// {
// particles_[i].init(x, y, xVel, yVel, lifetime);
// return;
// }
// }
//}
//객체 풀 클래스
class ParticlePool
{
public:
ParticlePool();
~ParticlePool() {}
public:
void create(double x, double y, double xVel, double yVel, int lifetime);
void animate();
private:
static const int POOL_SIZE = 100;
Particle particles_[POOL_SIZE];
Particle* firstAvailable;
};
//처음 풀을 생성하게 되면 모든 파티클이 사용가능하므로
//빈칸 리스트는 전체 풀을 관통해 연결된다.
ParticlePool::ParticlePool()
{
//처음생성 부분이니까 가장 첫번째를 머리로 한다.
firstAvailable = &particles_[0];
//모든 파티클을 순서대로 일단 연결한다.
for (int i = 0; i < POOL_SIZE - 1; ++i)
{
particles_[i].setNext(&particles_[i + 1]);
}
//마지막은 리스트의 null을 넣는다.
particles_[POOL_SIZE - 1].setNext(nullptr);
}
//이제 새로운 파티클을 생성하기 위해서는 첫번째 사용 가능한 파티클을 바로 얻어오면 된다.
//기존의 풀보다 검색 복잡도가 줄었다는..
void ParticlePool::create(double x, double y, double xVel, double yVel, int lifetime)
{
//얻은 파티클을 빈칸 목록에서 제거한다.
Particle* newParticle = firstAvailable;
//다음 빈칸을 머리에 연결한다.
firstAvailable = newParticle->getNext();
//새로 얻은 파티클 클래스를 초기화한다.
//여기서 초기화하면서 기존 파티클 클래스에 inUse 부분이 활성화되고 업데이트가 가능해지는것이다.
newParticle->init(x, y, xVel, yVel, lifetime);
}
void ParticlePool::animate()
{
for (int i = 0; i < POOL_SIZE; ++i)
{
particles_[i].animate();
//여기서 해당 파티클 클래스가 소멸되었다면..시간이 초과되었다면
//if(particles_[i].framesleft == 0) 이란 소리겠지..
//방금 죽은 파틸클을 빈칸 리스트의 앞에 추가한다. 앞이다 앞!!
//particles_[i].setNext(firstAvailable_);
//firstAvailable_ = &particles_[i];
}
}
이정도도 괜찮지만, 실제 제품 코드에서는 조금 부족하다.
1. 풀이 객체와 커플링 되는가?
객체 풀을 구현할 때에는 객체가 자신이 풀에 들어 있는지를 알게 할 것인지부터 결정해야 한다.
대부분은 그렇지만, 아무 객체나 담을 수 있는 일반적인 풀 클래스를 구현해야 한다면 이런 사치를 누리지 못할 수도
객체 풀 패턴은 경량 패턴과 비슷..