실험 목적
- 매 프레임 호출되는
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);
}
}
실험 방법
UpdateCoroutineManager컴포넌트에서 테스트 개수, 테스트 모드, 측정 시작 및 소요 프레임을 설정한다.- 유니티 에디터, Windows Mono 빌드, IL2CPP 빌드 환경에서 측정 결과를 확인한다.
실험 결과 - 1. 유니티 에디터
…
[1] Update

[2] Custom Update

[3] Coroutine - null

[4] Coroutine - WaitForEndOfFrame

실험 결과 - 2. Standalone Build(Mono)
…
[1] Update

[2] Custom Update

[3] Coroutine - null

[4] Coroutine - WaitForEndOfFrame

실험 결과 - 3. Standalone Build(IL2CPP)
…
[1] Update

[2] Custom Update

[3] Coroutine - null

[4] Coroutine - WaitForEndOfFrame

실험 결과 정리
| 환경 | 대상 | 프레임당 평균 소요 시간(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) 등으로 바꾸어 최적화를 이루었다면,
올바른 방향이니까 이 실험 결과를 보고 오해하지 않았으면 한다.