문제
유니티에서 코루틴을 사용할 때, 일정 시간 뒤에 코드가 실행되고 싶으면 YieldInstruction
을 사용한다.
예를 들면 yield return new WaitForEndOfFrame();
가 있다.
yield는 가비지를 생성하지 않지만 계속해서 새로운 YieldInstruction
오브젝트를 만들면 가비지가 생성된다.
실험
public class CoroutineTest : MonoBehaviour
{
private WaitForEndOfFrame WaitForEndOfFrame = new WaitForEndOfFrame();
[SerializeField] private int count = 10000;
[SerializeField] private bool useCache = true;
private void Start()
{
if (useCache)
{
for (int i = 0; i < count; i++)
{
StartCoroutine(CacheTest());
}
}
else
{
for (int i = 0; i < count; i++)
{
StartCoroutine(NoCacheTest());
}
}
}
private IEnumerator CacheTest()
{
yield return WaitForEndOfFrame;
}
private IEnumerator NoCacheTest()
{
yield return new WaitForEndOfFrame();
}
}
게임이 시작하면 코루틴을 count
개수만큼 생성하는 코드를 작성해 테스트해본다.
코루틴 내에서는 단순히 yield return
한다.
결과는 다음과 같다.
StartCoroutine()
자체도 가비지를 생성하기 때문에 둘 다 비교적 메모리 할당량이 높지만, 캐싱한 것을 사용했을 때가 GC Alloc이 좀 더 적은것을 확인할 수 있다.
※ 왜 코루틴을 호출하는 것 뿐만으로도 가비지가 생성될까?
C# 컴파일러는 코루틴 실행을 도와주는 클래스 인스턴스를 자동으로 생성하며, 유니티는 이 오브젝트를 사용해 코루틴의 상태를 추적하기 때문이다. 자세히는 코루틴의 로컬 변수가 yield 호출이 진행되는 동안 유지되어야 하기 때문에 이 생성된 클래스로 옮겨지며, 때문에 코루틴이 작동되는 동안 힙에 할당된 상태로 남아있게 된다. 또한 이 인스턴스는 코루틴의 내부 상태를 추적하여 yield 호출 이후에 코루틴의 어느 부분부터 다시 시작해야 하는지를 기억한다.
즉, 간단히 요약하자면 C# 컴파일러가 생성하는 코루틴의 실행 맥락을 기억하고 실행하게 도와주는 오브젝트때문이라고 볼 수 있다. 예를 들어 100,000개의 코루틴을 생성하면 100,000개의 맥락과 그 이상의 로컬변수들이 있는 오브젝트가 있을 것이니 그만큼 GC Alloc을 해줘야한다. 실제로 위 실험 코드에서 코루틴 내에 로컬 변수를 많이 만들면 그만큼 메모리 할당량도 늘어나는 것을 확인할 수 있다.
해결방안
다음과 같이 함수를 작성해서 YieldInstruction 오브젝트를 캐싱해서 사용한다.
public static class CoroutineHelper
{
private static readonly WaitForEndOfFrame WaitForEndOfFrame = new WaitForEndOfFrame();
private static readonly WaitForFixedUpdate WaitForFixedUpdate = new WaitForFixedUpdate();
private static Dictionary<float, WaitForSeconds> _WaitForSeconds;
public static WaitForSeconds WaitForSeconds(float seconds)
{
if(_WaitForSeconds.TryGetValue(seconds, out var waitForSeconds))
{
_WaitForSeconds.Add(seconds, waitForSeconds = new WaitForSeconds(seconds));
}
return waitForSeconds;
}
}
사용할땐 다음과 같이 사용하면 된다.
yield return CoroutineHeleper.WaitForEndOfFrame;
yield return CoroutineHeleper.WaitForSeconds(0.5f);
또한, StartCoroutine
을 빈번하게 호출하지 않는 것도 가비지를 덜 생성하게 하는 방법 중 하나이다.
출처
모바일 게임 성능 최적화: Unity 최고의 엔지니어가 전하는 프로파일링, 메모리, 코드 아키텍처 관련 팁 | Unity Blog