Update()
Update()는 어떻게 실행되나?
Monobehaviour을 상속한 클래스 내에서 Update() 메서드를 정의하면 해당 메서드는 매 프레임마다 유니티에 의해 실행된다.
그럼 어떻게? Update()메서드를 private로 선언하건, protected로 선언하건 항상 수행되는 것을 보면 리플렉션 따위를 써서 메서드 이름을 얻어오고 어딘가에 캐싱한 다음 매 프레임마다 호출할 것으로 예상해볼 수 있다. 이게 아무것도 없는 빈 Update()메서드를 정의하지 말아야 하는 이유이기도 할 것이다.
Update()의 성능은?
10000개의 Monobehaviour 객체를 매 프레임 업데이트 하고 싶다고 하자. 이 때 두 가지 방법이 가능하다.
하나는 Monobehaviour에 Update()를 정의하는 것.
private void Update()
{
i++;
}
다른 하나는 Manager 객체를 만든 뒤 이 객체가 매 프레임 Monobehaviour의 UpdateMe()를 호출하도록 하는 것.
private void Update() {
var count = list.Count;
for (var i = 0; i < count; i++) list[i].UpdateMe();
}
수행 시간은 Manager를 이용한 방식이 Mono에서는 약 4배, IL2CPP에서는 약 5배 빠르다.
만약 List가 아니라 Array를 사용한다면 Manager를 이용한 방식이 Mono에서는 8배, IL2CPP환경에서는 20배 빠르다.
왜 이런 차이가 날까?
당연히 Unity 내에서 Update()를 호출할 때 몇 가지 기능을 더 수행하기 때문이라고 볼 수 있다. 그러면 Unity는 Update를 호출할 때 뭘 더 할까?
1. SafeIterator : Behaviours를 순차적으로 업데이트 할 때, 유니티는 SafeIterator라는 iterator를 사용한다. 이건 리스트 순회 도중에 Behaviour가 삭제되어도 나머지가 정상적으로 실행될 수 있게 해준다.
2. 함수 호출을 해도 되는지 체크 : Start()메서드가 호출되고 정상적으로 초기화된 살아있는 GameObject에서 호출한 메서드인지 체크함.
3. 메서드 호출 준비 : native side에서 managed side의 메서드를 호출할 수 있도록 ScriptingInvocationNoArgs인스턴스를 만든다.
4. 메서드 호출 : arguments가 valid한지 체크함 (+IL2CPP 한정 메서드가 존재하는지 체크, IL2CPP 가상머신에서 메서드를 호출.)
결론?
사실 위 테스트에서의 Update()는 굉장히 단순하기 때문에 실행시간에서 저런 극적인 차이가 나는 것이다. 그렇기에 실제로는 별 차이가 안 날 수 있다. 그러나 업데이트하는 객체가 엄청 많다면? 그때는 무시못할 성능차이가 날 수 있다. 나중에 구조를 개선하려고 하면 힘들 것이다.
유니티에서 Update를 호출할 때, 호출이 Valid한지 체크하는 부분에서 많은 시간을 할애한다는 걸 알았다. 그러나 본인이 짠 코드가 어떻게 돌아가는지 잘 알고 있고 Valid체크가 필요 없다면 별도의 Manager를 통해 Update를 수행하는 게 좋을 수 있다.
참고: https://blog.unity.com/engine-platform/10000-update-calls