오브젝트 풀링이란 간단히 말하면 메모리를 미리 확보해놓고(pool) 필요한 오브젝트를 만들어서 넣어놓은 뒤에 오브젝트를 필요할 때 갖다 쓰는 오브젝트의 관리 방식이다.
왜 이런 방식을 사용할까? 그냥 생성하고 삭제만 잘 해주면 장땡 아닌가?
오브젝트 생성과 소멸은 너무 자주하면 문제가 되는데 바로 아래의 요약글을 보자.
오브젝트를 생성할 땐 새로 메모리를 새로 잡아주고 리소스를 끌어오거나 사용자가 붙여준 스크립트 등 가볍지 않은 초기화 활동이 일어난다.
아래는 힙 영역에 인스턴스를 만들 때 CPU에 어떠한 부담이 발생하는지 설명한다.
힙 영역의 공간을 할당하는 과정에서 오버헤드가 따른다.
메모리 할당은 상대적으로 비용이 높다고 한다.
할당할 공간을 찾고, 필요한 인스턴스 초기화 작업을 거쳐야하기 때문이다.
잦은 메모리 할당과 해제로 인해서 힙 메모리의 단편화를 야기할 수 있다.
메모리 공간이 작은 조각으로 나뉘어져서 공간은 많이 남았지만 사용은 할 수 없는 상태가 된다.
오브젝트를 소멸시킬 때에도 C#에서는 기본적으로 가비지 컬렉팅을 해주기 때문에 소멸 횟수가 많아질수록 CPU에 부담이 늘어난다.
가비지 컬렉션을 통해서 사용하지 않는 메모리를 정리한다.
인스턴스가 더 이상 참조되지 않으면 가비지 컬렉터는 해당 인스턴스를 메모리에서 제거한다.
가비저 컬렉션은 프로그램의 일시적 중단을 일으킬 수도 있다. (드물게)
오브젝트 풀링을 사용하면 과도한 메모리 할당/해제 작업을 피할 수 있으며, 가비지 컬렉션의 빈도도 줄어든다. 할당/해제가 줄어드니 메모리 단편화의 위험도 낮아지게 된다.
위 내용들은 ChatGPT에게 물어보고 나온 답변을 바탕으로 구글링을 하여 작성한 내용입니다.
오브젝트 풀링을 써야하는 이유만 써놓으면 재미 없다.
이 블로그 에서 큐브를 가지고 일반 생성/소멸 방식과 오브젝트 풀링 방식의 성능 차이를 실험해봤길래 나도 비슷한 느낌으로 시도해보았다.
public class Spawner : MonoBehaviour
{
float Timer;
public GameObject cube;
void Update()
{
Timer += Time.deltaTime;
if(Timer > 0.01f)
{
Instantiate(cube);
Timer = 0;
}
}
}
이렇게 0.01초가 초과하면 큐브를 생성한다.
public class TestCube : MonoBehaviour
{
private void OnEnable()
{
Destroy(gameObject, 4.0f);
float x = Random.Range(-10.0f, 10.0f);
float y = Random.Range(-10.0f, 10.0f);
float z = Random.Range(-10.0f, 10.0f);
transform.position = new Vector3(x, y, z);
}
}
큐브에 붙여준 스크립트이다.
생성되고 일정 범위 안에 랜덤으로 배치되며 4초 후에 소멸한다.
아래 GIF가 실행결과이다.
뭔가 멋있다..!
프레임은 낮을 땐 120 높을 땐 150 평소에는 140정도의 성능을 보여준다.
프레임당 경과한 시간은 6~7ms 정도이다.
public class PoolManager : MonoBehaviour
{
public GameObject cube;
public GameObject[] pool;
int index = 0;
int max = 400;
float Timer;
void Start()
{
pool = new GameObject[max];
for (int i=0; i< max; ++i)
{
Debug.Log(pool.Length);
GameObject newCube = Instantiate(cube,transform);
pool[i] = newCube;
newCube.SetActive(false);
}
}
void Update()
{
Timer += Time.deltaTime;
if (Timer >= 0.01f)
{
pool[index++].SetActive(true);
if(index >= max) index = 0;
Timer= 0;
}
}
}
우선 Start에서 길이 400의 오브젝트 배열을 만들어주고 비활성화된 오브젝트들로 꽉 채워준다.
그리고 0.01초 마다 배열의 인덱스를 증가시키며 오브젝트를 활성화시킨다.
public class TestCube2 : MonoBehaviour
{
private void OnEnable()
{
Invoke("InvokeActiveFalse", 4.0f);
float x = Random.Range(-10.0f, 10.0f);
float y = Random.Range(-10.0f, 10.0f);
float z = Random.Range(-10.0f, 10.0f);
transform.position = new Vector3(x, y, z);
}
void InvokeActiveFalse()
{
gameObject.SetActive(false);
}
}
큐브에 붙어있는 스크립트이다.
4초가 지나면 비활성화되도록 했다.
결과가..좀 애매하다.
프레임의 경우 낮을 땐 140, 높을 땐 170 평소에는 160정도로 나온다.
프레임당 경과시간은 6ms 초반대로 나온다.
분명 성능 향상은 있다. 오브젝트 풀링 방식이 프레임은 10정도 더 잘나오고 프레임당 경과 시간도 1ms 가깝게 차이가 난다.
기대만큼 극적인 성능향상은 아니다.
아마 CPU의 성능이 뛰어나서 큰 차이가 없던 것 같고,
오브젝트 풀링 방식은 배열을 순회하는 방식이기 때문에 if문이 추가되는 등의 로직이 추가됐기 때문이라고 생각한다.
그리고 코루틴을 사용하지 않고 update문에 의존한 점도 크게 작용하는 것 같다. 코루틴을 공부하고,,적용해보는 활동도 필요할 것 같다.
그래도 오브젝트 풀링 방식이 성능 면에서도 앞설 뿐만 아니라 잠재적인 문제들도 방지 할 수 있음은 확실하다.