Version: 2017.3
Unity 최적화에 대한 이해
메모리

프로파일링

성능을 개선하기 위한 모든 최적화 시도는 발견 프로세스로 시작해야 한다는 사실을 기억하십시오. 애플리케이션을 프로파일링하여 문제점을 파악하는 것이 첫 단계여야 하며, 그 이후 프로젝트의 기술적 사항과 에셋 아키텍처를 프로파일링 결과와 비교 분석해야 합니다.

참고: 이 장에서는 네이티브 코드 프로파일링 트레이스에 존재하는 메서드 이름을 다루는데, 이는 Unity 5.3 버전을 기반으로 합니다. 메서드 이름은 향후 Unity가 업데이트 되면 바뀔 수 있습니다.

Unity 개발자를 위해 다양한 프로파일링 툴이 준비되어 있습니다. Unity는 CPU 프로파일러, 메모리 프로파일러 그리고 새로 추가된 5.3 메모리 분석기 등과 같은 빌트인 툴을 갖추고 있습니다.

하지만, 최선의 결과를 얻으려면 다음과 같이 플랫폼 별로 특화 툴을 사용하는 편이 좋습니다.

  • iOS: InstrumentsXCode Frame Debugger

  • Android: Snapdragon Profiler

  • Intel CPU/GPU를 사용하는 플랫폼: VTuneIntel GPA

  • PS4: Razor 모음

  • Xbox: Pix

이 툴은 일반적으로 프로젝트의 C++ 버전을 만들기 위해 IL2CPP를 활용할 수 있는 플랫폼에서 가장 유용합니다. 네이티브 코드 버전은 Mono에서 실행할 때는 쓸 수 없는 투명한 콜스택과 고해상도 메서드 타이밍을 제공합니다.

Unity는 iOS 게임을 프로파일링하는 툴에 대한 기본 가이드를 작성했습니다. Profiling with Instruments를 참조하십시오.

시작 트레이스 분석

이 두 가지 키 메서드도 반드시 이것을 사용해야만 트레이스할 수 있는 것은 아니기 때문입니다. 이는 프로젝트의 설정, 에셋, 코드가 시작 시간에 영향을 주로 주는 부분이기 때문입니다.

플랫폼마다 시작 시간이 상이하게 나타난다는 점을 상기하십시오. 대부분의 플랫폼에서는 정적 스플래시 화면으로 사용자에게 표시됩니다.

위의 스크린샷은 iOS 디바이스에서 실행되는 샘플 프로젝트의 Instruments 트레이스입니다. 플랫폼 종속적인 startUnity 메서드에 있는 UnityInitApplicationGraphicsUnityLoadApplication 메서드에 주목하십시오.

UnityInitApplicationGraphics는 그래픽스 디바이스 설정이나 Unity 내부 시스템 초기화와 같은 다양한 내부 작업을 실행합니다. 이와 더불어 리소스 시스템 역시 초기화합니다. 이들 작업을 진행하려면, 리소스 시스템에 있는 모든 파일의 색인을 우선 로드해야 합니다.

“Resources” 이름을 가지는 모든 폴더에 포함된 에셋 파일이 리소스 시스템 데이터에 포함됩니다. (1) (참고: 이는 프로젝트의 “Assets” 폴더에 위치하는 “Resources” 폴더와 그 자식 폴더에만 적용됩니다.) 따라서 리소스 시스템을 초기화하는 데 걸리는 시간은, 거의 정비례하게, “Resources” 폴더에 있는 파일의 개수에 따라 증가합니다.

UnityLoadApplication은 프로젝트의 첫 씬을 로드하고 초기화하는 메서드를 포함합니다. 이는 셰이더 컴파일링, 텍스처 업로드, 게임 오브젝트 인스턴스화와 같이 첫 씬을 표시하는 데 필요한 모든 데이터를 역직렬화하고 인스턴스화하는 작업을 진행합니다. 이와 더불어, 첫 씬의 모든 MonoBehaviour는 이 시점에서 Awake 콜백을 실행합니다.

이 과정은, 만약 프로젝트에서 첫 씬의 Awake 콜백에 오래 걸리는 코드가 있는 경우 해당 코드로 인하여 프로젝트 전체의 초기 시작 시간을 지연시킬 수 있다는 점을 보여줍니다. 이를 해결하기 위해서는 느린 해당 코드를 제거하거나 애플리케이션의 다른 부분에서 실행되도록 해야 합니다.

런타임 트레이스 분석

초기 시작 시간 이후 캡처된 프로파일링 트레이스의 경우, 주로 고려할 곳은 PlayerLoop 메서드입니다. 이것은 Unity의 메인 루프이며 매 프레임당 한번씩 실행하는 코드입니다.

위의 스크린샷은 Unity 5.4 샘플 프로젝트의 프로파일링을 실행한 것이며, PlayerLoop의 가장 주목할 만한 몇 개의 메서드를 보여줍니다. PlayerLoop의 메서드 이름은 Unity 버전에 따라 다를 수 있다는 점을 기억하십시오.

PlayerRender는 Unity의 렌더링 시스템을 실행하는 메서드입니다. 이는 오브젝트 컬링, 동적 배치 계산, GPU에 드로우 명령 전달 등과 같은 작업을 진행합니다. 모든 이미지 이펙트나 렌더링 기반 스크립트 콜백(OnWillRenderObject 등) 역시 이 곳에서 실행됩니다. 일반적으로, 메서드는 프로젝트가 상호작용을 하는 동안 CPU 사용 시간을 가장 많이 소모합니다.

BaseBehaviourManager는 3개의 CommonUpdate 템플릿 버전을 호출합니다. 이는 현재 씬에서 액티브 게임 오브젝트에 포함된 MonoBehaviours의 특정 콜백을 호출합니다.

  • CommonUpdate<UpdateManager>Update 콜백을 호출합니다.

  • CommonUpdate<LateUpdateManager>LateUpdate 콜백을 호출합니다.

  • CommonUpdate<FixedUpdateManager>는 물리 시스템을 활성화한 경우 FixedUpdate 콜백을 호출합니다.

일반적으로 BaseBehaviourManager::CommonUpdate<UpdateManager> 메서드는 검사해야 할 가장 흥미로운 메서드입니다. 왜냐하면, 이 메서드는 Unity 프로젝트에서 작동하는 대부분의 스크립트 코드에 대한 진입 지점이기 때문입니다.

다음과 같이 흥미있는 여러 다른 메서드도 있습니다.

UI::CanvasManager는 프로젝트가 Unity UI를 사용하는 경우 다양한 콜백을 호출합니다. 이는 Unity UI의 배치 계산 및 레이아웃 업데이트를 포함하며, 이 두 개의 작업은 Unity 프로파일러에 매우 많은CanvasManager가 나타나도록 합니다.

DelayedCallManager::Update는 코루틴을 실행합니다. 이 메서드는 “코루틴” 장에 더 자세하게 설명되어 있습니다.

PhysicsManager::FixedUpdate는 PhysX 물리 시스템을 실행합니다. 이는 우선 PhysX의 내부 코드를 실행하며, 리지드바디와 콜라이더 같은 현재 씬의 물리 오브젝트 수에 영향을 받습니다. 하지만, 물리 기반 콜백 특히 OnTriggerStayOnCollisionStay 역시 여기에 나타납니다.

만일 프로젝트가 2D 물리 시스템을 사용하는 경우, 이는 Physics2DManager::FixedUpdate 에서 유사한 호출 집합으로 나타납니다.

스크립트 메서드 분석

만일 스크립트가 IL2CPP로 교차 컴파일된 플랫폼에서 호출되는 경우 ScriptingInvocation 오브젝트를 포함하는 트레이스 줄을 찾아보십시오. 여기가 스크립트 코드를 실행하기 위해서 Unity의 내부 네이티브 코드가 스크립트 런타임으로 전환하는 지점입니다. (2) (참고: 기술적으로는, IL2CPP를 통하여 실행된 경우 C#/JS 스크립트 코드는 네이티브 코드가 됩니다. 하지만, 교차 컴파일된 코드는 우선적으로 메서드를 IL2CPP 런타임 프레임워크를 통하여 실행하며, 이는 손으로 작성한 C++코드와는 유사하지 않습니다.)

위의 스크린샷은 Unity 5.4 에서 실행되는 샘플 프로젝트에서 가져온 것 입니다. RuntimeInvoker_Void 줄 아래에 위치한 모든 메서드는 프레임당 한 번 실행되는 교차 컴파일된 C# 스크립트 부분입니다.

트레이스 라인은 비교적 이해하기 쉽습니다. 각각의 메서드는 오리지널 클래스의 이름과 오리지널 메서드의 이름 방식으로 명명되었습니다. 이 예제에서는, EventSystem.Update, PlayerShooting.Update와 같은 다수의 Update 메서드를 볼 수 있습니다. 이는 대부분의 MonoBehaviour에 있는 기본 Unity Update 콜백입니다.

이러한 메서드를 확장하면 내부에서 어떤 메서드가 CPU 시간을 소모하는지 파악할 수 있습니다. 이는 프로젝트의 다른 스크립트 메서드, Unity API와 C# 라이브러리 코드를 포함합니다.

위의 트레이스는 터치 이벤트가 UI 요소 위에 올라왔는지 또는 활성화하는지를 감지하기 위해 StandaloneInputModule.Process 메서드로 프레임당 한 번씩 전체 UI를 레이캐스팅 하고 있는 것을 보여줍니다. 주로 CPU 시간을 차지하는 것은 UI 요소를 전부 반복하는 작업과, 마우스의 포지션이 경계 사각형에 있는지 테스트하는 작업입니다.

에셋(Asset) 로드

에셋 로드 역시 CPU 트레이스에서 파악할 수 있습니다. 에셋 로드를 나타내는 주된 메서드는 SerializedFile::ReadObject입니다. 이 메서드는 바이너리 데이터 스트림을 Unity의 직렬화 시스템에 연결합니다. 이 직렬화 시스템은 Transfer 라는 메서드를 통해 작동합니다. Transfer 메서드는 텍스처, MonoBehaviour, 파티클 시스템과 같은 모든 에셋 종류에 존재합니다.

위의 스크린샷에서는 한 개의 씬을 로드하고 있습니다. 이는 Unity가 씬의 모든 에셋을 읽고 비직렬화하는 과정을 포함하며, 이들 작업은 SerializedFile::ReadObject 아래 다양한 Transfer 메서드가 호출하고 있는 것을 볼 수 있습니다.

일반적으로, 만일 런타임 도중 성능이 저하되고, 성능 트레이스 결과 SerializedFile::ReadObject가 상당한 시간을 차지하고 있는 것으로 나타난다면 에셋 로드로 인하여 프레임 속도가 저하됩니다. 대부분의 경우 SerializedFile::ReadObject 메서드는 동기화된 에셋 로드가 SceneManager, Resources, 또는 AssetBundle API를 통해 요청되는 경우에만 메인 스레드에 존재합니다.

이런 종류의 성능 저하는 일반적인 방법을 통해 해결할 수 있습니다. 에셋 로딩을 비동기화 시키거나 (이는 무거운 ReadObject 호출을 워커 스레드에 넘기게 됩니다), 특정 무거운 에셋을 미리 로드할 수도 있습니다.

Transfer 호출은 오브젝트를 복사할 경우에도 나타납니다(이는 트레이스에서 CloneObject 메서드로 나타납니다). 만약 Transfer 호출이 CloneObject 호출 아래에서 나타나는 경우, 에셋은 저장소에서 로드되지 않습니다. 대신, 이전 오브젝트의 데이터가 새로운 오브젝트로 전송됩니다. 이를 위해서, Unity는 이전 오브젝트를 직렬화하고, 결과 데이터를 비직렬화하여 새로운 오브젝트로 만듭니다.

각주

  • (1) 이는 프로젝트의 Assets 폴더에 위치한 Resources 폴더와 그 자식 폴더인 Resources 폴더에만 적용됩니다.
  • (2) 기술적으로는, IL2CPP를 통go 실행된 경우 C#/JS 스크립트 코드는 네이티브 코드가 됩니다. 하지만, 교차 컴파일된 코드는 우선적으로 메서드를 IL2CPP 런타임 프레임워크를 통하여 실행하며, 이는 손으로 작성한 C++코드와는 유사하지 않습니다.
Unity 최적화에 대한 이해
메모리