유니티 가비지 컬렉션 최적화하기 3 – 번역
유니티에서 가비지 컬렉션 최적화하기 3
– 원문 링크 –
가비지 컬렉션에 의한 피해 줄이기 (Reducing the impact of garbage collection)
넓은 의미에서, 가비지 컬렉션이 게임에 미치는 영향을 줄이는 방법에는 3가지 방법이 있습니다:
- 가비지 컬렉터를 실행하는데 소모되는 시간을 줄일 수 있습니다.
- 가비지 컬렉터가 실행되는 빈도를 줄일 수 있습니다.
- 가비지 컬렉터를 의도적으로 실행시켜서, 로딩 화면 등 퍼포먼스가 중요하지 않은 시점에 가비지 컬렉터가 실행되도록 하는 것입니다.
위의 3가지 방법을 염두해두면, 가비지 컬렉션의 영향을 줄일 수 있는 3가지 전략이 있습니다:
- 힙 할당과 오브젝트 참조가 적게 발생하도록 게임을 구성합니다. 힙 상의 오브젝트와 검사해야 할 오브젝트 참조가 적으면 가비지 컬렉션이 발생할때, 실행하는데 소모되는 시간을 줄일 수 있습니다.
- 퍼포먼스가 중요한 시점에 힙 할당 및 해제를 줄입니다. 할당과 해제가 적으면 가비지 컬렉션의 발생 빈도를 줄일 수 있습니다. 이 방법은 힙 파편화도 줄입니다.
- 가비지 컬렉션과 힙 확장을 의도한 시점에 발생하도록 구성합니다. 이를 통해서, 가비지 컬렉션과 힙 확장이 예측한 시점과 이들의 영향을 받지 않는 시점에 발생하도록 합니다. 이 방법은 구현이 더 어렵고, 덜 확실한 방법이지만, 전반적인 메모리 관리 전략의 일부로 사용하면 가비지 컬렉션에 의한 영향을 줄일 수 있습니다.
생성되는 가비지의 양 줄이기 (Reducing the amount of garbage created)
생성되는 가비지를 코드에서 줄일 수있는 몇가지 방법에 대해서 살펴보겠습니다.
캐싱 (Caching)
코드에서 힙 할당을 야기하는 함수를 반복적으로 호출하고, 그 결과를 버리면 불필요한 가비지가 발생합니다.
그 대신, 이런 오브젝트를 가리키는 참조 값을 저장하고 재사용하는 방법을 사용합니다. 이 기법을 캐싱이라고 합니다.
다음 예제에 있는 코드는 새로운 배열을 생성하기때문에, 호출될때마다 힙 할당을 발생시킵니다.
void OnTriggerEnter(Collider other) { Renderer[] allRenderers = FindObjectsOfType<Renderer>(); ExampleFunction(allRenderers); }
힙 할당을 한번만 발생시키고 그 참조값이 저장됩니다. 저장된 배열은 언제든지 가비지를 생성시키지 않고 재사용 가능입니다.
private Renderer[] allRenderers; void Start() { allRenderers = FindObjectsOfType<Renderer>(); } void OnTriggerEnter(Collider other) { ExampleFunction(allRenderers); }
자주 호출하는 함수안에서 할당하지 마십시오 (Don’t allocate in functions that are called frequently)
MonoBehaviour에서 힙 메모리를 할당할때, 가장 안좋은 방법은 자주 호출되는 함수 안에서 할당하는 것입니다.
예를 들어, Update()와 LateUpdate()는 매 프레임마다 호출됩니다. 따라서, 이 함수에서 가비지를 생성하도록 코드를 작성하면, 이렇게 생성된 가비지가 아주 빨리 쌓이게 됩니다.
이 경우, 가능하다면, 이 오브젝트의 참조 값을 Start()또는 Awake()에서 캐싱하는 방법을 고려해보는 것이 좋습니다.
또는 할당을 발생시키는 코드가 꼭 필요할때만 실행되도록 구성하는 것이 좋습니다.
상황이 바뀔 때만 코드가 실행되도록 작성한, 이동 코드의 예를 살펴보겠습니다. 다음 코드에서는, Update()에서 할당을 발생시키는 함수를 호출해서, 가비지가 자주 발생합니다:
void Update() { ExampleGarbageGeneratingFunction(transform.position.x); }
transform.position.x의 값이 변경되었을 때만 할당을 발생시키는 함수가 호출되도록 코드를 변경했습니다. 이제, 매 프레임 할당이 발생하지 않고, 필요할 때만 할당이 발생합니다.
private float previousTransformPositionX; void Update() { float transformPositionX = transform.position.x; if (transformPositionX != previousTransformPositionX) { ExampleGarbageGeneratingFunction(transformPositionX); previousTransformPositionX = transformPositionX; } }
Update()함수에서 가비지를 줄이는 다른 방법 중 하나는 타이머를 사용하는 것입니다. 이 방법은 가비지를 생성하는 코드가 매 프레임 실행될 필요는 없고, 정기적으로 실행되어야 하는 경우에 적합합니다.
다음 예제 코드에서, 가비지를 생성시키는 함수는 매 프레임마다 호출됩니다.
void Update() { ExampleGarbageGeneratingFunction(); }
다음 코드에서는, 타이머를 사용해서 가비지를 생성시키는 함수가 1초당 한번씩 호출되도록 했습니다.
private float timeSinceLastCalled; private float delay = 1f; void Update() { timeSinceLastCalled += Time.deltaTime; if (timeSinceLastCalled < delay) { ExampleGarbageGeneratingFunction(); timeSinceLastCalled = 0f; } }
자주 실행되는 코드에 대해서, 이렇게 간단한 변경만으로도 생성되는 가비지의 양을 현저하게 줄일 수 있습니다.
컬렉션 지우기 (Clearing collections)
새 컬렉션을 생성하면 힙 할당이 발생합니다. 코드에서 새 컬렉션을 한번 이상 생성하는 경우, 이 컬렉션의 참조 값을 캐시에 저장하고, new 키워드를 반복적으로 호출하는 대신, Clear() 함수를 이용해서 내용을 비웁니다.
void Update() { List myList = new List(); PopulateList(myList); }
다음 예제에서는, 컬렉션이 생성되거나 컬렉션의 크기를 조절해야하는 경우에만 할당이 발생합니다. 이렇게 하면 생성되는 가비지의 양을 현저하게 줄일 수 있습니다.
private List myList = new List(); void Update() { myList.Clear(); PopulateList(myList); }
오브젝트 풀링 (Object pooling)
스크립트에서 할당을 줄이더라도, 런타임에서 다수의 오브젝트를 생성하고, 삭제하면 여전히 가비지 컬렉션의 문제에서 자유로울 수 없습니다.
오브젝트 풀링(Object Pooling)은, 반복적으로 생성하고 삭제해야하는 오브젝트의 재사용을 통해서, 할당과 해제를 줄이는 기법입니다.
오브젝트 풀링은 게임에서 광범위하게 사용되며, 오브젝트를 자주 스폰(Spawn)하고 삭제해야하는 상황에 가장 적합한 방법입니다. 총에서 총알을 발사하는 경우를 예로들 수 있습니다.
오브젝트 풀링에 대한 전체 가이드는 이 글의 범위를 벗어나지만, 실제로 매우 유요한 기술이며, 배울 가치가 있는 기법입니다.
유니티 Learn 사이트의 오브젝트 풀링에 대한 강좌는, 유니티에서 오브젝트 풀링 시스템을 구현하는데 좋은 가이드입니다.
(저의 블로그 링크에 가시면 오브젝트 풀링을 구현하는 강좌가 준비되어 있으니 참고하시기 바랍니다.)
내용 끝까지 읽어주셔서 감사합니다.
배너 클릭은 저에게 많은 힘이 됩니다.
감사합니다 🙂