Search Unity

저는 EMEA 컨설팅 및 개발 팀의 DRE(Developer Relations Engineer)로서 유니티의 주요 고객사를 방문하여 프로젝트 성능 문제를 해결하는 데 가장 많은 시간을 할애합니다. 성능 최적화에 관심이 있다면 이번 블로그 포스팅에서 알려드리는 내용을 실제 프로젝트에 적용해보세요.

유나이트 코펜하겐 2019에서 저는 ‘실무에서 사용하는 성능 최적화’라는 세션을 맡아 발표를 진행했습니다. 이 세션은 유니티의 베스트 프랙티스 가이드 내용을 숙지했지만 자체적으로 프로파일링 및 분석 툴을 사용하여 기타 성능 문제를 진단하고 해결할 수 있는 실무 지식을 보유하지 못한 중급 수준의 Unity 개발자를 위해 진행되었습니다.

강연에서 다뤘던 내용은 다음과 같습니다.

  • DRE의 기본적인 역할과 주요 업무(프로젝트 검토)에 대한 개략적인 설명
  • 최적화와 프로파일링 소개
  • CPU, GPU 및 메모리 소모 최적화에 대한 섹션 각 1개 – 섹션마다 실제로 발생했던 문제 사례 2개와 문제를 해결하는 데 사용한 툴 및 기술 요약 설명
  • 일반적인 최적화 규칙들
  • Q&A

동영상은 아래를, 강연 슬라이드는 여기를 참조하세요.

모든 내용을 상세히 다루기에는 세션 시간이 부족했지만 동영상에 담긴 분들 외에도 수많은 유나이트 참석자 분들과 강연 이후에 유익한 대화를 나누었습니다. 이번 블로그 포스팅에서는 세션이 끝나고 나눈 이야기를 공유하고자 합니다. 포스팅을 읽기 전 세션을 먼저 시청하실 것을 권해드립니다.

프로젝트 검토

우리 업무의 핵심은 프로젝트 검토입니다. 프로젝트 검토를 위해 고객의 사무실을 방문하여 보통 이틀 동안 고객이 진행하고 있는 프로젝트를 숙지하고, 여러 질문을 하면서 프로젝트의 요구 사항과 디자인 관련 결정 사항을 파악하고, 다양한 프로파일링 도구를 사용하여 성능 병목 현상을 찾아냅니다. 빌드 시간이 짧고 효과적으로 설계된 프로젝트(모듈식 씬, AssetBundle을 많이 사용한 경우 등)의 경우에는 현장에서 변경 사항을 적용하고 프로파일을 재구성하여 새로운 문제를 파악합니다. 빌드 시간을 최적화하면 반복 작업을 더 많이 수행할 수 있습니다. 특히 모바일 기기와 게임 콘솔과 같이 타겟 하드웨어가 개발 당시에 사용된 하드웨어와 매우 다른 프로젝트의 경우라면 빌드 시간이 짧을수록 좋습니다.

고객사는 각자 매우 다양한 특성을 지니고 있으며, 프로젝트 유형 또한 광범위한 플랫폼과 요구 사항을 포함하므로 프로젝트 검토 과정은 제각기 다릅니다. 고객사 방문 중 해결할 수 없는 복잡한 문제가 있는 경우, 최대한 많은 정보를 파악한 후 사무실로 돌아와 추가적인 조사를 진행합니다. 필요한 경우 유니티 R&D 부서의 전문 개발자에게 조언을 구합니다. 결과물은 고객의 요구에 따라 다르지만, 보통 문제점을 요약하고 권장 사항을 기술한 서면 보고서의 형태로 제공됩니다. 프로젝트 검토 과정의 핵심이자 목표는 항상 고객에게 가장 큰 가치를 주는 결과물을 제공하는 것입니다.

또한 검토 시 Unity의 소스 코드에 액세스할 수 있지만, 가급적 고객과 같은 입장에서 공개 프로파일링 도구와 베스트 프랙티스를 활용하여 프로젝트를 최적화합니다. 성능 문제의 원인을 보다 근본적으로 파악하기 위해 코드를 살펴보게 될 경우, 새롭게 파악한 사실을 Unity 기술 자료에 업로드하여 모든 사용자가 새로운 정보를 이용할 수 있도록 하고 있습니다.

CPU 기반과 GPU 기반의 비교

세션 중 말씀드린 것처럼 프로젝트를 최적화하려면 먼저 실제 병목 현상을 찾아야 합니다. 한 가지 방법은 Unity 프로파일러를 통해 CPU 사용 내역을 확인하는 것입니다. 아래 이미지에서 보는 바와 같이 대부분의 프레임 시간이 렌더링에 소요되었다면 CPU와 GPU 중 어느 것을 주로 사용하는지 파악해야 합니다.

렌더링은 CPU와 GPU를 모두 사용하는 프로세스입니다. 본문에서 이 프로세스의 전반을 설명할 수는 없지만, 요약하자면 씬의 렌더링은 다음 단계로 구성됩니다.

  1. 머티리얼을 공유하는 오브젝트의 그룹별로 다음 과정이 수행됩니다.
    1.  CPU가 GPU로 커맨드를 전송하여 GPU의 내부 상태(예: 셰이더, 기반 텍스처, 버텍스 포맷 등)를 설정합니다. 이 단계를 ‘set pass’ 호출이라고도 합니다.
    2.  CPU는 GPU로 지오메트리 배치를 전송하고 1.A 단계에서 설정한 상태를 사용하여 렌더링합니다. 이 단계를 ‘드로우 콜’이라고도 하며, 여기에는 상당히 많은 비용이 소요됩니다.
    3. 동일한 머티리얼 유형의 지오메트리를 더 렌더링해야 하는 경우 1.B 단계로 진행합니다.

위의 알고리즘에는 보다 세부적인 내용과 유의점이 있지만, 중요한 사실은 렌더링이 CPU와 GPU 사이에서 이루어지는 작업이라는 점입니다. 아래의 스크린샷에서 보는 바와 같이 Xcode와 같은 툴을 통해 두 리소스에서 실제로 소요되는 시간을 상세하게 확인할 수 있습니다.

이러한 정보는 Unity 프로파일러에서도 확인할 수 있지만, GPU 지표는 그래픽 카드와 드라이버의 지원 사항에 따라 표시되지 않을 수도 있습니다.

프로파일링 툴을 통해 CPU 및 GPU의 소요 시간을 확인할 수 없다면 Unity 프로파일러에서 임의의 프레임을 확인해보면 됩니다. 만약 Gfx.WaitForPresent에 대한 호출이 존재하며 ‘호출에 상당한 시간이 소요됨(call is taking a considerable amount of time)’이라는 메시지가 표시된다면 CPU가 GPU에서 모든 렌더링 커맨드를 처리 완료할 때까지 대기한다는 의미이며, 이를 GPU바운드(GPU에 묶여 있는 상태)라고 합니다. WaitForTargetFPSGfx.PresentFrame 등 여러 마커의 의미는 이 매뉴얼 페이지를 참조하세요.

GPU 워크로드에 영향을 미치는 요소는 다음과 같이 다양합니다.

  • 필 레이트(Fill rate): 애플리케이션이 한 프레임에 과도한 수의 픽셀을 여러 번 입힙니다. 이 프로세스를 ‘오버드로우’라고 합니다.
  • 메모리 대역폭: 애플리케이션이 다량의 텍스처 데이터를 GPU로 전송합니다. 아틀라싱 등을 통해 텍스처 수를 줄이거나 텍스처 크기 자체를 줄이고, 가능한 경우 압축 포맷으로 설정하면 이 현상을 완화할 수 있습니다.
  • 버텍스 프로세싱: 애플리케이션이 GPU로 과도한 수의 지오메트리를 전송합니다. 이 상황은 유나이트 세션에서 다루었습니다.

반면 CPU를 주로 사용하는 경우(CPU바운드) 물리, 게임플레이 코드 등 여러 이유로 인해 CPU의 처리 시간이 지연될 수 있으므로 프로파일러를 확인해야 합니다. 프로파일러를 확인한 결과 렌더링에 많은 시간이 소요되는 것으로 나타나면 CPU가 GPU로 과도한 커맨드를 전송하느라 부하가 걸리는 상황일 수 있습니다. 이러한 경우 상태 변경(또는 ‘SetPass’ 호출)의 수와 배치 수를 줄여서 최적화할 수 있습니다. 관련 내용을 더 자세히 알아보려면 ‘성능 문제 해결하기’ 튜토리얼을 참조하세요.

사례 연구: 데이터 로드 CPU 성능 불안정 발생

프로젝트에서 자주 발생하는 성능 문제는 애플리케이션 시동 단계나 새로운 레벨 전환 시 발생하는 끊김 현상입니다. 이러한 끊김 현상은 Unity 프로파일러에서 성능 불안정으로 나타납니다.

이러한 문제는 주로 많은 비용이 소요되는 계산 및 대규모 메모리 할당으로 인해 발생합니다. 이 예에서는 아래 스크린샷에서 보듯 CPU 성능 불안정으로 인해 10초에 가까운 지연과 관리되는 할당 3.8GB가 발생했습니다.

이러한 성능 불안정은 두 가지 이유로 문제가 됩니다. 첫 번째로 성능 불안정이 너무 길게 지속되면 애플리케이션의 흐름에 방해가 됩니다. 로딩 화면을 사용하면 CPU 성능 불안정으로 인해 발생한 지연을 ‘숨길’ 수는 있으나, 애니메이션화된 요소를 화면에 표시해야 하는 경우는 로딩 과정에서 애니메이션도 지연되므로 효과적인 방법은 아닙니다. 두 번째로는 이러한 성능 불안정으로 인해 발생하는 대규모 할당은 관리되는 힙의 규모를 영구적으로 증가시킵니다. Unity의 자동 메모리 관리 시스템은 참조되지 않은 메모리를 이어지는 할당에서 재사용하지만, 관리되는 힙의 전체 크기는 감소하지 않고 증가하기만 합니다. 이러한 현상을 ‘비압축 가비지 컬렉션(non-compacting garbage collection)’이라고 합니다. 자세한 내용은 기술 자료Unity Learn 웹사이트를 참조하세요.

이러한 성능 불안정에는 보통 여러 요인이 복합적으로 영향을 미칩니다. 필드에 표시되는 내용에 따르면 이러한 현상은 애플리케이션이 최적화되지 않은 포맷(예: JSON 또는 XML)으로 데이터를 저장하며, 파서가 콘텐츠 처리를 위해 상당량의 메모리를 할당해야 하기 때문에 발생합니다. 대부분의 경우 이러한 할당과 더불어, 앞서 언급한 데이터 처리 및 관련 메모리 할당에 요구되는 무거운 계산이 주요 원인이 됩니다.

이와 같은 문제를 완화하기 위해 유니티는 일반적으로 고객에게 ‘budgeted time manager’ 시스템을 구현하도록 권장해드립니다. 이 시스템은 프레임당 한도 내에서 오브젝트를 인스턴스화 및 초기화하고 바이너리 포맷을 지원하며, 여러 개의 프레임에 걸쳐 부담을 분산합니다. 또한 바이너리 포맷 지원으로 인해 할당의 크기가 최소화됩니다.

모든 데이터를 하나의 메서드에 로드하는 대신 ‘ budgeted time manager ‘를 사용하는 방법은 일반 가비지 컬렉터와 점진적 가비지 컬렉터의 차이와 유사합니다. 즉, 전자는 관리되는 오브젝트가 모두 처리되기까지 프레임을 지연시키는 반면, 후자는 여러 프레임에 걸쳐 작업을 분산시킵니다.

바이너리 포맷은 그 특성상 개발 과정에서 다루기가 더 까다롭습니다. 따라서 텍스트 포맷 지원을 모두 제거하지 않는 것이 좋습니다. 대신 두 포맷을 모두 지원하고 애플리케이션의 개발 버전을 실행할 때에는 텍스트 포맷을, 릴리스 버전을 실행할 때에는 바이너리 포맷을 사용할 것을 권장합니다.

가비지 컬렉션에 관한 추가 사항

빠른 속도의 게임에서 발생하는 GC 성능 불안정 예시에서는 점진적 가비지 컬렉터를 활성화하고 프레임 시간을 최대한 단축하여 프레임 종료 시마다 알고리즘이 작동할 수 있는 공간을 확보하도록 조언했습니다. 단, 세션에서 충분히 강조하지 않은 중요한 사실이 있습니다. 점진적 가비지 컬렉터를 사용하는 경우에도 관리되는 메모리 할당의 양과 크기를 가능한 한 최소화해야 합니다. 일반 가비지 컬렉터에 비해 점진적 가비지 컬렉터는 워크로드가 여러 프레임으로 분산되어 관리되는 오브젝트 전체가 처리될 때까지 프레임이 지연되지 않으므로 안정적인 프레임 속도를 확보한다는 이점이 있습니다.

가비지 컬렉터를 비활성화하려면 스크립트에서 GarbageCollector.GCMode의 정적 필드 값을 GarbageCollector.Mode.Disabled로 설정하면 됩니다.

이 기법은 가비지 컬렉션 알고리즘 처리에 리소스를 소모하지 않으려는 경우 유용합니다. 단, 이 방법을 사용하려면 가비지 컬렉터 비활성화 시 할당이 발생하지 않아야 합니다. 세션에서 언급한 바와 같이, 메모리 사용량이 한도를 초과하는 경우 운영 체제(특히 Android나 iOS와 같은 모바일 플랫폼)에서 애플리케이션을 자동으로 종료하기 때문입니다.

사례 연구: 권한 서버가 있는 FPS

몇 개월 전 헤드리스(headless) 모드로 실행 중인 권한 서버 아키텍처가 있는 멀티플레이어 1인칭 슈팅 게임의 프로젝트를 검토한 적이 있습니다. Unity 메모리 프로파일러를 이용하여 메모리 캡처를 실시한 결과, 헤드리스 서버에 필요하지 않은 메시, 라이트 프로브, 오디오 클립, 메시 렌더러 및 기타 여러 오브젝트에 수백 메가바이트가 할당된 것을 확인할 수 있었습니다.

물론 이처럼 불필요한 메모리 소모가 발생해도 1인 멀티플레이어 세션 실행은 가능하지만, 프로젝트 규모 확대에는 분명히 제약이 따랐습니다. 즉, 서버에서 활성 인스턴스 수를 늘리려면 필요한 메모리 용량이 대폭 증가해야 했습니다.

이 사례에서는 고객에게 모든 게임 레벨 씬을 두 부분으로 나누어 별도의 에셋 번들에 저장하도록 조언했습니다. 여기서 첫 번째 엔티티는 헤드리스 서버에서 필요한 모든 정보를 포함하는 ‘논리 씬’이며, 두 번째 엔티티는 클라이언트만 사용하는 모든 정보를 포함하는 ‘비주얼 씬’입니다.

단, 이러한 구분으로 인해 일부 경우 아티스트와 레벨 디자이너가 하나의 씬에서 작업하지 못하는 문제가 발생할 수 있습니다. 따라서 콘텐츠 크리에이터의 워크플로를 방해하기보다는 빌드 프로세스에서 ‘논리적 씬’과 ‘비주얼 씬’으로 구분하도록 권장했습니다.

프로파일링 프로파일러 마커

앞서 다룬 내용에 따르면 애플리케이션의 핵심 루프에서는 프레임당 할당을 0에 가깝게 줄여야 합니다. 그러면 가비지 컬렉션 알고리즘에 의해 발생하는 오버헤드가 대폭 감소합니다. 이러한 작업에 가장 적합한 툴은 Unity 프로파일러지만, 보고되는 콜 스택의 기본 뎁스 수준은 애플리케이션의 스크립팅 코드에서 엔진 네이티브 코드 호출에 따른 첫 번째 콜 스택 뎁스로 한정됩니다(예: MonoBehaviour.Start(), MonoBehaviour.Update() 및 유사한 메서드). 즉, 실제로 스크립트가 일반적인 경우처럼 다른 스크립트의 메서드를 호출하면 관리되는 할당이 어디서 발생하는지 정확히 식별할 수 없습니다.

이러한 문제를 해결하는 한 가지 방법은 스크립트에 프로파일러 마커를 추가하는 것입니다. 그러면 프로파일링 프로세스에서 더 많은 정보가 기록되므로 할당의 소스 범위를 좁힐 수 있습니다.

또 다른 방법은 딥 프로파일링입니다. Unity Learn 웹사이트의 튜토리얼에서 딥 프로파일링을 실행하는 방법을 참조하실 수 있습니다. 딥 프로파일링을 실시하면 다량의 오버헤드가 발생하여 애플리케이션이 매우 느려지므로 보고된 시간의 정확도가 낮아집니다. 따라서 딥 프로파일링을 비활성화한 상태로 프로파일링 세션을 실행하고, 관리되는 할당이 의도치 않게 발생한 시나리오를 기록합니다. 그리고 보고된 콜 스택이 할당의 소스를 추적할 수 있을 정도로 상세하지 않은 경우에만 딥 프로파일링을 활성화한 상태로 두 번째 세션을 실시하여 할당의 소스를 찾도록 권장합니다.

Unity 2019.3 이전 버전에는 딥 프로파일링이 Mono 스크립팅 백엔드 사용 시에만 지원되었습니다. Unity 2019.3 베타에서는 이러한 제약이 사라지고 Mono와 IL2CPP 백엔드에서 모두 지원합니다. 릴리스 노트에 따르면 다음과 같은 업데이트가 추가되었습니다.

프로파일러: Mono 및 IL2CPP 플레이어에 딥 프로파일러 지원이 추가되었습니다.
프로파일러: 플레이어에 딥 프로파일링 지원 빌드 옵션이 추가되었습니다. 딥 프로파일링을 지원하는 플레이어를 구축하면 C# 코드 계측을 동적으로 활성화하거나 비활성화할 수 있습니다.
프로파일러: 플레이어에 관리되는 할당 콜스택 지원이 추가되었습니다. 콜스택 컬렉션을 활성화하면 GC.Alloc 샘플에 C# 코드 콜스택이 포함됩니다.

이제 IL2CPP 백엔드에 딥 프로파일링이 지원되므로 개발자는 iOS처럼 IL2CPP만 지원하는 플랫폼에서 딥 프로파일 캡처를 실시할 수 있습니다. 또한 플레이어에 관리되는 할당 콜 스택 지원이 추가되어 개발자가 딥 프로파일링을 실시하지 않고도 할당의 소스를 찾을 수 있습니다.

다음 단계

성능 최적화는 매우 광범위한 주제입니다. 기반 하드웨어의 작동 방식뿐 아니라 제한 사항까지 이해할 수 있는 역량 수준은 물론, Unity가 지원하는 다양한 클래스 및 컴포넌트, 알고리즘과 데이터 구조, 프로파일링 툴 사용 방법 등 다양한 기술에 대한 이해를 요구합니다. 또한 디자인 요구 사항을 충족하면서도 효율적인 솔루션을 찾기 위해서는 창의력도 필수적입니다.

댓글을 통해 더 자세히 알아보고 싶은 최적화 관련 주제를 알려주세요. Unity를 최대한 효과적으로 사용하실 수 있도록 지원하겠습니다.

7 replies on “실무에서 사용하는 성능 최적화”

I did a search for “budgeted time manager” and could find zero references to it. Any examples anywhere showing how to organize the loading of objects using this technique?

I did a search for “budgeted time manager” and could find zero references to it. Any examples anywhere showing how to organize the loading of objects using this technique?

How about managing a large size project?
Unity Editor starts to crawl as project size grows and it’s the main pain point.
The least we can do is to offload many assets outside of the Assets(as addressable or something) folder and use references.
The static model objects would be good candidates for offloading.
They will be managed independently from the project and builds automatically when asked by different Platform or Editor versions.

This way, many assets works like shared libraries and we all can work with lean project.

Comments are closed.