오브젝트 풀링

강성원·2023년 12월 28일
0

Unity

목록 보기
9/12
post-thumbnail

오브젝트 풀링이란?

오브젝트 풀링이란 간단히 말하면 메모리를 미리 확보해놓고(pool) 필요한 오브젝트를 만들어서 넣어놓은 뒤에 오브젝트를 필요할 때 갖다 쓰는 오브젝트의 관리 방식이다.

왜 이런 방식을 사용할까? 그냥 생성하고 삭제만 잘 해주면 장땡 아닌가?

오브젝트 생성과 소멸은 너무 자주하면 문제가 되는데 바로 아래의 요약글을 보자.

1. 잦은 오브젝트 생성의 문제

오브젝트를 생성할 땐 새로 메모리를 새로 잡아주고 리소스를 끌어오거나 사용자가 붙여준 스크립트 등 가볍지 않은 초기화 활동이 일어난다.
아래는 힙 영역에 인스턴스를 만들 때 CPU에 어떠한 부담이 발생하는지 설명한다.

1-1. 메모리 할당 시 오버헤드

힙 영역의 공간을 할당하는 과정에서 오버헤드가 따른다.
메모리 할당은 상대적으로 비용이 높다고 한다.
할당할 공간을 찾고, 필요한 인스턴스 초기화 작업을 거쳐야하기 때문이다.

1-2. 메모리 단편화

잦은 메모리 할당과 해제로 인해서 힙 메모리의 단편화를 야기할 수 있다.
메모리 공간이 작은 조각으로 나뉘어져서 공간은 많이 남았지만 사용은 할 수 없는 상태가 된다.

2. 잦은 오브젝트 소멸의 문제

오브젝트를 소멸시킬 때에도 C#에서는 기본적으로 가비지 컬렉팅을 해주기 때문에 소멸 횟수가 많아질수록 CPU에 부담이 늘어난다.

2-1. 가비지 컬렉션

가비지 컬렉션을 통해서 사용하지 않는 메모리를 정리한다.
인스턴스가 더 이상 참조되지 않으면 가비지 컬렉터는 해당 인스턴스를 메모리에서 제거한다.
가비저 컬렉션은 프로그램의 일시적 중단을 일으킬 수도 있다. (드물게)

오브젝트 풀링을 사용하면 위 문제들이 해소되는가?

오브젝트 풀링을 사용하면 과도한 메모리 할당/해제 작업을 피할 수 있으며, 가비지 컬렉션의 빈도도 줄어든다. 할당/해제가 줄어드니 메모리 단편화의 위험도 낮아지게 된다.

위 내용들은 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문에 의존한 점도 크게 작용하는 것 같다. 코루틴을 공부하고,,적용해보는 활동도 필요할 것 같다.

그래도 오브젝트 풀링 방식이 성능 면에서도 앞설 뿐만 아니라 잠재적인 문제들도 방지 할 수 있음은 확실하다.

profile
개발은삼순이발

0개의 댓글