Posts 유니티 - 매 프레임 처리 성능 테스트 - Update() vs 코루틴
Post
Cancel

유니티 - 매 프레임 처리 성능 테스트 - Update() vs 코루틴

실험 목적


  • 매 프레임 호출되는 Update() 메소드, 코루틴의 성능 비교


실험 조건


  • 운영체제 : Windows 10
  • 유니티 에디터 버전 : 2020.3.17f1
  • 실행 환경 : 유니티 에디터, Windows Standalone Build(Mono, IL2CPP)


실험 대상


[1] Update()

  • 각 컴포넌트마다 Update() 작성
UpdateEveryFrame.cs
1
2
3
4
public class UpdateEveryFrame : MonoBehaviour
{
    private void Update() { }
}


[2] CustomUpdate()

  • 한 컴포넌트의 Update()에서 다른 컴포넌트들의 CustomUpdate() 호출
CustomUpdateCaller.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class CustomUpdateCaller : MonoBehaviour
{
    private static CustomUpdateCaller singleton;
    private List<CustomUpdateCallee> list = new List<CustomUpdateCallee>(100000);

    public static void AddElement(CustomUpdateCallee element)
    {
        singleton.list.Add(element);
    }

    private void Awake()
    {
        singleton = this;
    }

    private void Update()
    {
        foreach (var item in list)
        {
            item.CustomUpdate();
        }
    }
}
CustomUpdateCallee.cs
1
2
3
4
5
6
7
8
public class CustomUpdateCallee : MonoBehaviour
{
    private void OnEnable()
    {
        CustomUpdateCaller.AddElement(this);
    }
    public void CustomUpdate() { }
}


[3] Coroutine - null

  • 무한 반복문 내에서 yield return null을 통해 매프레임 코루틴 검사
CoroutineEveryFrame.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class CoroutineEveryFrame : MonoBehaviour
{
    private void Start()
    {
        StartCoroutine(CoRoutine());
    }

    private IEnumerator CoRoutine()
    {
        while (true)
        {
            yield return null;
        }
    }
}


[4] Coroutine - WaitForEndOfFrame

  • 무한 반복문 내에서 yield return new WaitForEndOfFrame()을 통해 매프레임 코루틴 검사
CoroutineEveryFrame.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class CoroutineEveryFrame : MonoBehaviour
{
    private void Start()
    {
        StartCoroutine(CoRoutine());
    }

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


실험 통제 컴포넌트


UpdateCoroutineManager.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
public class UpdateCoroutineManager : MonoBehaviour
{
    private enum TestMode { Update, CustomUpdate, CoroutineNull, CoroutineEndOfFrame }

    [SerializeField] private int testCount = 100000;
    [SerializeField] private TestMode mode = TestMode.Update;

    [SerializeField] private int startFrame = 250;
    [SerializeField] private int frameCount = 100;

    private void Awake()
    {
        switch (mode)
        {
            case TestMode.Update:
                CreateUnits<Test_UpdateEveryFrame>();
                break;

            case TestMode.CustomUpdate:
                GameObject go = new GameObject("GO");
                go.AddComponent<Test_CustomUpdateCaller>();
                CreateUnits<Test_CustomUpdateCallee>();
                break;

            case TestMode.CoroutineNull:
                CreateUnits<Test_CoroutineEveryFrame>();
                break;

            case TestMode.CoroutineEndOfFrame:
                CreateUnits<Test_CoroutineEndOfFrame>();
                break;
        }

        void CreateUnits<T>() where T : MonoBehaviour
        {
            for (int i = 0; i < testCount; i++)
            {
                GameObject go = new GameObject("GO");
                go.hideFlags = HideFlags.HideInHierarchy;
                go.AddComponent<T>();
            }
        }
    }

    private float timeBegin;

    private void Update()
    {
        if (Time.frameCount == startFrame)
        {
            Log($"Start : {Time.frameCount}");
            timeBegin = Time.realtimeSinceStartup;
        }
        else if (Time.frameCount == (startFrame + frameCount))
        {
            float elapsedMS = (Time.realtimeSinceStartup - timeBegin) * 1000f / frameCount;
            Log($"Average(ms) : {elapsedMS:F2}");

#if UNITY_EDITOR
            UnityEditor.EditorApplication.isPaused = true;
#endif
        }
    }

    private void Log(string log)
    {
        logString = log;
    }

    private string logString = "";
    private GUIStyle style;

    private void OnGUI()
    {
        if (style == null)
        {
            style = new GUIStyle(GUI.skin.box);
            style.fontSize = 48;
            style.alignment = TextAnchor.MiddleCenter;
        }

        Rect r = new Rect();
        r.x = Screen.width * 0.1f;
        r.y = Screen.height * 0.1f;
        r.width = Screen.width * 0.8f;
        r.height = Screen.height * 0.2f;

        GUI.Box(r, logString, style);
    }
}


실험 방법


  1. UpdateCoroutineManager 컴포넌트에서 테스트 개수, 테스트 모드, 측정 시작 및 소요 프레임을 설정한다.
  2. 유니티 에디터, Windows Mono 빌드, IL2CPP 빌드 환경에서 측정 결과를 확인한다.


실험 결과 - 1. 유니티 에디터


[1] Update

image

[2] Custom Update

image

[3] Coroutine - null

image

[4] Coroutine - WaitForEndOfFrame

image


실험 결과 - 2. Standalone Build(Mono)


[1] Update

image

[2] Custom Update

image

[3] Coroutine - null

image

[4] Coroutine - WaitForEndOfFrame

image


실험 결과 - 3. Standalone Build(IL2CPP)


[1] Update

image

[2] Custom Update

image

[3] Coroutine - null

image

[4] Coroutine - WaitForEndOfFrame

image


실험 결과 정리


환경 대상 프레임당 평균 소요 시간(ms)
Unity Editor Update() 67.89
Unity Editor Custom Update 33.99
Unity Editor Coroutine - null 134.09
Unity Editor Coroutine - WaitForEndOfFrame 168.63
Mono Build Update() 19.83
Mono Build Custom Update 17.15
Mono Build Coroutine - null 68.48
Mono Build Coroutine - WaitForEndOfFrame 80.99
IL2CPP Build Update() 17.34
IL2CPP Build Custom Update 17.17
IL2CPP Build Coroutine - null 61.12
IL2CPP Build Coroutine - WaitForEndOfFrame 88.01


매 프레임 한 번씩 호출되는 경우,

어떤 플랫폼에서든 항상 Update()가 코루틴보다 성능이 좋다.


주의사항


간혹 위 실험 결과를 오해하는 경우가 있는데,

Update()를 통해 매 프레임 호출 vs 코루틴으로 0.1초마다 호출

이와 같은 경우에 대해 위 실험 결과를 대입해서는 안된다.

‘동일하게 매 프레임마다 한 번씩 호출하는 경우’에 대해서만 단순히 참고할 수 있는 정도로 이해하면 된다.


그러니까, 어차피 매 프레임마다 호출하는 데다가 관리상 문제도 없는 경우,

굳이 Update()를 통해 호출하던 것을 코루틴으로 바꿀 필요는 없다는 의미다.


오히려 Update()로 매프레임 호출하여 병목이 발생하던 지점을

코루틴의 yield return WaitForSeconds(float) 등으로 바꾸어 최적화를 이루었다면,

올바른 방향이니까 이 실험 결과를 보고 오해하지 않았으면 한다.

This post is licensed under CC BY 4.0 by the author.