300x250

코루틴은 은근히 쓰레기 메모리(가비지)를 자주 생성해요. 코루틴을 자주 사용하는 로직에서 이를 최적화하지 않는다면 GC의 collect에서 프레임이 상당히 떨어질 수 있어요..

코루틴에서 가비지가 생성되는 주요한 부분으로는 크게 두 가지가 있어요..

 

- StartCoroutine

- YieldInstruction

StartCoroutine  메서드를 호출할 때 유니티가 해당 코루틴을 관리하기 위해 엔진내부에서 인스턴스가 생성되며 이때 이것이 가비지가 된다고 해요. StartCoroutine은 유니티 엔진 내부의 코드이기 때문에 이를 최적화하는 것은 쉽지 않기 때문에 코루틴 기능을 직접 제작하거나 비슷한 기능을 제공하는 에셋 More Effective Coroutine을 사용해야 한다고 해요.

 

웬만하면 최소한으로 StartCoroutine을 사용해야 할 것 같아요.

 

그렇다면 저희가 최적화를 건드릴 수 있는 부분은 YieldInstruction가 남아있어요. YieldInstruction 이거는 아래와 같은 종류들이 있어요.

 

여기서 yield구문 자체는 가비지를 생성하지 않지만 new가 붙는 yield return new WaitForSeconds(float) 같은 것들이 가비지가 생성이 되기 때문에 캐싱을 해주는 게 좋아요.

 

using System.Collections;
using UnityEngine;

public class Test2Main : MonoBehaviour
{
    private void Start()
    {
        StartCoroutine(this.WaitForSecondsTestImpl());
        StartCoroutine(this.WaitForEndOfFrameTestImpl());
        StartCoroutine(this.WaitForFixedUpdateTestImpl());
    }

    private IEnumerator WaitForSecondsTestImpl()
    {
        while (true)
        {
            yield return new WaitForSeconds(0.01f);
        }
    }

    private IEnumerator WaitForEndOfFrameTestImpl()
    {
        while (true)
        {
            yield return new WaitForEndOfFrame();
        }
    }

    private IEnumerator WaitForFixedUpdateTestImpl()
    {
        while (true)
        {
            yield return new WaitForFixedUpdate();

        }
    }
}

스샷을 보시면 프레임이 끝날 때마다 WaitForEndOfFrameTestImpl에서 가비지가 생성되고, FixedFrame이 끝날 때마다 WaitForFixedUpdateTestImpl에서 가비지가 생성되고, 지정한 시간이 지날 때마다 WaitForSecondsTestImpl에서 가비지가 생성이 되고 있는 것을 볼 수 있어요.

 

이때 캐싱을 할 때 WaitForEndOfFrame, WaitForFixedUpdate은 고정된 값이기에 한 번만 생성해 놓으면 되지만 WaitForSeconds처럼 여러 개의 Seconds가 필요할 때엔 고정된 값이 아니어서 제가 참고한 사이트에선 딕셔너리에 Seconds를 Key로 넣어놓고 값에 Seconds에 해당하는 WaitForSeconds를 넣어놓는 형식으로 캐싱을 했어요.

 

이때 기존에 있는 Key인지 검사하는 과정에서 딕셔너리에 TryGetValue를 사용할 때 박싱이 일어나는 것 때문에 IEqualityComparer를 구현해서 딕셔너리를 생성할 때 object형식으로 비교하는 게 아닌 제네릭형식으로 비교하는 것을 직접 만드셨던데

 

[Unity] Dictionary.TryGetValue() 박싱테스트 :: 별빛상자 (tistory.com)

 

제가 테스트해본 결과 제가 작업하고 있는 환경에선 TryGetValue에서 박싱이 일어나고 있지 않기 때문에 IEqualityComparer는 구현을 하지 않아도 됐어요.

using System.Collections.Generic;
using UnityEngine;

public class YieldInstructionCache
{
    class FloatComparer : IEqualityComparer<float>
    {
        bool IEqualityComparer<float>.Equals(float x, float y)
        {
            return x == y;
        }
        int IEqualityComparer<float>.GetHashCode(float obj)
        {
            return obj.GetHashCode();
        }
    }

    public static readonly WaitForEndOfFrame WaitForEndOfFrame = new WaitForEndOfFrame();
    public static readonly WaitForFixedUpdate WaitForFixedUpdate = new WaitForFixedUpdate();

    private static readonly Dictionary<float, WaitForSeconds> timeInterval = new Dictionary<float, WaitForSeconds>();
    private static readonly Dictionary<float, WaitForSecondsRealtime> timeIntervalReal = new Dictionary<float, WaitForSecondsRealtime>();


    public static WaitForSeconds WaitForSeconds(float seconds)
    {
        WaitForSeconds wfs;
        
        if (!timeInterval.TryGetValue(seconds, out wfs))
            timeInterval.Add(seconds, wfs = new WaitForSeconds(seconds));

        return wfs;
    }

    public static WaitForSecondsRealtime WaitForSecondsRealTime(float seconds)
    {
        WaitForSecondsRealtime wfsReal;

        if (!timeIntervalReal.TryGetValue(seconds, out wfsReal))
            timeIntervalReal.Add(seconds, wfsReal = new WaitForSecondsRealtime(seconds));

        return wfsReal;
    }
}

 

이 부분이 전 필요 없었어요. 혹시라도 나중에 버전이 달라졌을 때 박싱이 발생한다면 IEqualityComparer를 사용해야 할 것 같네요.

 

using System.Collections;
using UnityEngine;

public class Test2Main : MonoBehaviour
{
    private void Start()
    {
        StartCoroutine(this.WaitForSecondsTestImpl());
        StartCoroutine(this.WaitForEndOfFrameTestImpl());
        StartCoroutine(this.WaitForFixedUpdateTestImpl());
    }


    private IEnumerator WaitForSecondsTestImpl()
    {
        while (true)
        {
            yield return YieldInstructionCache.WaitForSeconds(0.1f);
        }
    }

    private IEnumerator WaitForEndOfFrameTestImpl()
    {
        while (true)
        {
            yield return YieldInstructionCache.WaitForEndOfFrame;
        }
    }

    private IEnumerator WaitForFixedUpdateTestImpl()
    {
        while (true)
        {
            yield return YieldInstructionCache.WaitForFixedUpdate;

        }
    }

}

더 이상 가비지가 생성이 안되고 있는 모습을 볼 수 있어요.

 

저는 유니티 사용할 때 코루틴을 엄청 많이 사용했었는데 지금까지 이런 걸 하나도 고려 안 한 상태에서 했었어요. 많이 반성하게 되네요! 

 

저는 유니티 버전 2021.3.5f1, .NET Standard 2.1, Visual Studio 2019 환경에서 테스트했어요.

 

 

참고자료 :

유니티 코루틴 최적화 (ejonghyuck.github.io)

그때는 맞고 지금은 틀리다 - 제네릭 컬렉션도 박싱이 발생할 수 있다 | Overworks’ lab in GitHub

C# Dictionary ContainsKey와 TryGetValue 뭐가 더 효율적인가? (tistory.com)

[Unity] yield return 종류 — KKIMSSI (tistory.com)

 

300x250