2015년 5월 3일 일요일

[Unity3D] 유니티에서 최적화 단계 밟기


여기서는 다른곳에서 이미 거론된 Unity3D메모리 최적화에 관련된 이야기를 더 하지 않는다.
texture는 npot를 쓰지말라 라는 등의 이미 나돌고 있는 이야기들은 기본적으로 숙지해야할 이야기들이니, 다른곳을 참조하시라. 개인적으로는 Unity3d를 첫 게임 개발 엔진으로 접한 개발자들에게 도움이 되지 않을까 하는 생각을 한다.

1. Garbage 트레이싱

Profiler를 이용, deep profiler를 사용하여 garbage가 발생하는 구간을 특정해 찾아낸다.

주로 봐야할 부분은 대체적으로 주기적으로 garbage가 축적되어 발생하는 부분들
예를들자면 애니메이션 재생을 위한 스크립트 등에서 주기적으로 garbage가 발생하는 구간.

마냥 garbage를 줄이기 위해 보이는대로 줄여나가기보다는 주기적인 garbage발생처를 우선순위로 생각하자.

대체적으로 작은곳에 함정들이 존재하는데, 매우 작은 garbage가 주기적으로 생성되는 부분이나
key-value boxing이 매우 많은 양이 발생해 묵직한 덩어리가 되는때가 있다.
이런 부분에서는 boxing에 주의가 필요하고, 단순 임시 배열들 (mesh에 대한 vertex입력을 위한 일시 배열 등)은 단순하게라도 pooling을 실시하자.

2. Scene Memory 총량 관리

대체적으로 1개 scene단위로 모든 표시될 가능성이 있는 객체들은 하나도 빠짐없이 scene로딩중에 마치는것이 옳다. 그렇지 않아도 도중에 instantiate할 수 있겠으나. instantiate자체에 현 시점에서는 async로 동작하는 내용이 있지 않기에 덩치가 큰 내용물을 생성할경우 필연적으로 렉이 발생한다.

처음에 숨겨둔 객체들을 다 켜서 메모리의 걱정이 될 정도라면 아에 처음부터 그 scene에 들어가지 않게끔 물량 제어를 하라. 어차피 유저가 모두 한번씩 이용을 한다고 생각하면 최종적으로는 모두 사용될 메모리다.
gc이기에, 잠깐 메모리에 적재했다가 안쓴다고 바로 release한다는 플로우는 생각할 수 없다. gc.collect를 강제로 부른다면 렉의 주범이 될것이고. 반복되는 alloc-dealloc은 최적화에서는 모두 제거되어야할 대상이다.

대체적으로 렉의 주범이 되는 것들은 instantiate의 물량 그 자체와
awake에 맞물려있는 초기화 코드들때문.
디자인 하기 나름이긴 하겠으나, 가급적 Start에서 coroutine을 사용하지 말라.
이는 키자마자 끊어버리면 처음 스택 이후에는 실행 안될 가능성이 높아진다.
나중에 사용할 오브젝트들을 disable상태로 어딘가에 pooling하는 것을 전제로 생각하고
awake에서 비 활성화 상태의 초기화 동작을 모두 마친 후, pooling으로 비 활성화중이던 객체를 생성하라.

Scene단위의 메모리 체크는 특정 scene에 진입한 이후 Profiler - memory - (Simple->Detailed)에서 객체별로 확인하자.

Texture의 용량도 그렇지만 SerializedField에 해당하는 Mono의 개체수도 생각하자. 상상 이상으로 커다란 덩치를 자랑한다. ScriptableObject의 Serialization과 동일한 기능을 이용하고 있을것이라 생각되는데, int 1개에 대해 3KB정도의 용량을 소모하고 있었던 것으로 기억한다. (소스를 보진 않은터라 장담할 순 없으나, 만개정도 생성한후, 비어있는 Mono와의 차이를 쟀을때 이정도였던것으로 기억한다)
운용상에 생긴 garbage등도 포함되어 있으리라.

3. pooling

대체적으로 UI를 보면 주요 이슈가 되는건 이른바 무한 스크롤리스트, 말하자면 pooling scroll list라 할 수 있겠다. 리스트의 종류도 여럿 있는 경우가 대부분이니 (친구, 선물, 퀘스트 등등) 리스트의 entity view구현에 pooling을 제어할 수 있는 인터페이스를 포함해 구현하라. 개인적으로는 오브젝트가 늘어난다고 풀의 구현체를 여럿 만들지는 않는다. 하나로 끝내두고 이를 generic화 한다. gameobject라면 관리용 view에 대해 이를 실시한다.

대체적으로 OnActivate, OnDeactivate, OnRefresh의 이벤트 핸들링이 필요하더라.

4. UI의 객체는 View<->Model로 분할된 코드 디자인으로

사실 이 부분은 호 불호가 갈리는 부분이다. 개인적으로 pooling이 가능한 UI용 객체를 디자인할때, MVVM의 View-Model data binding이 가장 스마트한 방법이라 생각한다. 개인적으론 이 부분의 솔루션으로 Reactive Extension을 이용하고 있으며, 유니티에는 UniRx라는 본가 Rx의 handport 라이브러리가 존재한다. 한번은 타 프레임워크에서 접했었고, 이후에 두어번즈음 독자적인 MVVM을 작성해봤는데 UniRx자체는 신뢰해도 될만한 퀄리티의 라이브러리라 생각된다.

View와 Model을 분할함으로서 처음 디자인할때 다소 복잡하게 느껴지지만 앱 디자인 전체로 봤을때는 나중에 어떤 데이터 처리를 해서 UI상에 반영한다는 상황이 왔을때 일 자체가 심플하게 끝나고 코드 디자인에도 큰 영향을 미치지 않는다.

더불어 pooling을 해야하는 상황이 왔을때 더욱 빛을 발휘한다. view에 대해 자신이 표시할 데이터의 참조만 바꿔주면 끝나기 때문. 대체적으로 view에 대해서는 어떤 데이터를 보러가라는 식으로(table, id)디자인을 하고, 이 레퍼런스를 옮겨주기만 하면 view에 대한 반영이 끝난다. 같은 객체를 재사용해야하는 pooling에 있어서는 보다 일을 심플하게 마칠 수 있다.

5. LINQ사용시의 주의점들

LINQ는 어느 사용자들간에 있어서는 기피 대상으로 자리잡고 있다.
사용은 편하지만 느리다는 말들이 많다. 확실히 극단적인 비교의 예로 Dictionary와 비교를 하자면 느릴수 밖에 없다. 모든 검색이 순차검색인 Linear search에 기반하고 있고, 생각해보면 한 항목마다의 조건부 제어이기 때문에 그럴수밖에 없다.

모든 탐색코드를 LINQ로 실시하란 말은 하지 않는다. 오히려 한번 필터링을 거친 데이터는 필요에 따라 ToDictionary()등의 메소드를 이용해 적절한 자료구조에 배치해야한다.
심지어 foreach는 끝단의 필터링을 마친 데이터에 대해서는 한번은 사용하는게 맞다.
기존 기능의 완전 대체가 아닌 적재 적소에 사용해야할 기능이라 생각하자.

LINQ를 기반으로 한 Functional Programming을 하게되면 코드가 짧아지는 경향이 있을 뿐만 아니라, code에 딱히 주석이 없더라도 어떤 일을 하고 있는 코드인지를 한눈에 알 수 있게된다.

Reactive Extension에서도 IObservable에 대해 LINQ를 사용하고 있는데, 기본적으로 IObservable은 데이터 스트림이긴 하지만 한순간에 큰 데이터가 날아오는 경우가 없기때문에 문제가 되지 않는다.

다만, 현 시점에서 Unity3D의 System.Linq;는 사용하지 않는게 좋다. 그게 iOS빌드라면 더더욱 그렇다. AOT build에서 컴파일 되지 않는 코드가 구버전 Mono Implementation에 존재하고 있고, 현재 Unity3D는 이 해결된 코드가 올라와있지 않다.

대안으로는 https://github.com/RyotaMurohoshi/UniLinq 이 git-repos의 AOT safe한 라이브러리를 대안으로 이용하자, 기존 LINQ코드들을 똑같이 이용할 수 있다. Beta표기라 걱정인가? 문제가 생기면 오픈소스이기에 내부를 들여다 볼 수도 있을것이고. 막상 소스를 살펴보면 일일히 소스에서 for로 찾아가던때가 상기될 것이다. 또한 상기 라이브러리는 개인 개발자의 hand-port-ish가 아니다. 위 기능은 문제가 해결된 mono의 코드에서 발췌한 것이라 들었다.

LINQ의 사용 의의는 코드의 퀄리티의 증진이다. 쉽게 말하자면 읽기 쉬운 코드를 쓰자는 말이다.
비즈니스 로직을 구성하면 끝단의 컨텐츠 구성은 하드코딩일 수밖에 없는데,
그 와중에도 읽기 편한 코드를 구성하려면 이런 기능들을 숙지해 나가는 노력이 필요하다 생각한다.