10
16
모바일 게임 성능 최적화: Unity 최고의 엔지니어가 전하는 프로파일링, 메모리, 코드 아키텍처 관련 팁
위 링크의 글에 대한 요약입니다.

개발 초기부터 타겟 기기에서 자주 프로파일링 실행하기

개발 초기부터 프로젝트를 프로파일링하여 여러 성능 문제를 즉시 발견하기(프로젝트의 성능 시그니처를 개발)

가능하면 타겟 기기에서 개발 빌드를 프로파일링하기

프로파일러의 작동 방식 이해하기

CPU 및 메모리 트랙을 기본적으로 활성화하여 살펴보기

물리를 많이 사용하는 게임이나 음악에 기반한 게임플레이를 개발 중이라면
필요에 따라 렌더러, 오디오, 물리와 같은 보조 프로파일러 모듈을 모니터링할 수 있다.

Build Settings > Development Build 및 Autoconnect Profiler를 체크하여
기기에 애플리케이션을 빌드하거나 수동으로 연결하여 앱 시작 시간을 단축할 수 있다.

스파이크 현상을 살펴볼 때는 물리, AI, 애니메이션 등의 고비용 작업을
먼저 검토한 다음 가비지 컬렉션을 살펴본다.
창을 클릭하여 특정 프레임을 분석할 때 타임라인 또는 계층 구조 뷰를 사용한다.

타임라인
: CPU 바운드 또는 GPU 바운드 여부를 판단
    Gfx.WaitForCommands 마커
    : 렌더 스레드가 준비되었으나 메인 스레드에 병목 현상이 일어날 가능성을 의미(CPU 바운드)
    Gfx.WaitForPresent 마커
    : 메인 스레드는 준비되었으나 GPU가 프레임을 표시하기를 기다리는 중이었음을 의미(GPU 바운드)

계층 구조
: 프레임의 관리되는 힙 메모리(GC Alloc) 확인

힙의 불필요한 할당은 GC(가비지 컬렉션) 스파이크 유발하므로 주의

불필요한 문자열 생성 또는 조작 줄이기

1. JSON, XML과 같은 문자열 기반 데이터 파일은 구문 분석하지 않는 것이 좋다.
2. 데이터를 ScriptableObjects 또는 MessagePack이나 Protobuf 같은 형식으로 대신 저장한다.
3. 런타임에서 문자열을 빌드해야 하는 경우 StringBuilder 클래스를 사용한다.
유니티 함수 호출

1. 일부 함수는 힙 할당을 생성하는데, 참조를 루프 도중에 할당하지 말고 배열에 저장하자.
2. 가비지 생성을 방지하는 함수들을 활용하자. 예를 들어 문자열을 GameObject.tag와 직접 비교하는 대신
   GameObject.CompareTag를 사용하는 방법이 있다.(전자는 새로운 문자열 반환으로 가비지를 생성함)
박싱

1. 참조 유형 변수 대신 값 유형 변수를 전달하지 않도록 하자.
2. 전달하려는 값 유형에 구체적인 오버라이드(제너릭 등)를 사용하자.
코루틴

yield는 가비지를 생성하지 않지만 새로운 WaitForSeconds 오브젝트를 만들면 가비지가 생성된다.
yield 라인에서 생성하는 대신 WaitForSeconds 오브젝트를 캐시하고 재사용하자.
LINQ 및 정규식

두 가지 모두 박싱에서 가비지를 생성한다.
성능이 문제라면 LINQ와 정규식을 사용하지 말자.
새로운 배열을 만드는 대신 for 루프와 리스트를 사용하자.
GC 측정은 System.GC.Collect로 확인

Project Settings > Player 에서 Use incremental GC(점진적 가비지 컬렉터)를 체크하여
프로그램 실행 중에 한 번 길게 중단되는 것이 아니라
훨씬 짧은 중단이 여러 프레임에 걸쳐 여러 번 나타나게 할 수 있다.

매 프레임에 실행되는 코드 최소화

불필요한 로직을 Update, LateUpdate, FixedUpdate에서 제외

반드시 Update를 사용해야 한다면 아래처럼 n개 프레임마다 코드를 실행하는 방안도 검토
private int interval = 3;

void Update()
{
    if (Time.frameCount % interval == 0)
    {
        ExampleExpensiveFunction();
    }
}

Start/Awake에서 대규모 로직 사용 방지

Awake, OnEnable, Start 함수에서 고비용 로직을 사용할 경우 로딩시간이 길어진다.

빈 Unity 이벤트 방지

빈 MonoBehaviours도 리소스를 필요로 하므로 비어 있는 Update 또는 LateUpdate 메서드는 제거한다.

Debug Log 구문 제거하기

Update, LateUpdate 또는 FixedUpdate에 있는 Log 구문은 성능을 낮출 수 있다.
빌드를 만들기 전에 Log 구문을 비활성화하자.
전처리(PreProcess) 지시문과 함께 조건부 속성으로 만들어도 좋다.
public static class Logging
{
    [System.Diagnostics.Conditional("ENABLE_LOG")]
    static public void Log(object message)
    {
        UnityEngine.Debug.Log(message);
    }
}

문자열 파라미터 대신 해시 값 사용하기

Unity는 내부적으로 애니메이터나 머티리얼, 셰이더 프로퍼티를 식별할 때 문자열 이름을 사용하지 않는다.
빠른 처리를 위해 모든 프로퍼티 이름이 프로퍼티 ID에 해시되어 있으며,
해당 ID가 실제로 프로퍼티를 식별하는 데 사용된다.
애니메이터, 머티리얼 또는 셰이더에서 Set 또는 Get 메서드를 사용하는 경우에는
문자열 값 메서드 대신 정수 값 메서드를 사용한다.
문자열 메서드 역시 문자열 해싱을 수행한 다음 해시된 ID를 정수 값 메서드로 전달하기 때문이다.
애니메이터 프로퍼티 이름에는 Animator.StringToHash를,
머티리얼 및 셰이더 프로퍼티 이름에는 Shader.PropertyToID를 사용하자.
Animator.StringToHash 사용 예시
Shader.PropertyToID 사용 예시

런타임 시 컴포넌트 추가 방지

런타임에 AddComponent를 호출하였을 때 비용이 발생하므로,
이미 설정된 원하는 컴포넌트로 프리팹을 인스턴스화하는 것이 더 효과적이다.

게임 오브젝트 및 컴포넌트 캐시

GameObject.Find, GameObject.GetComponent, Camera.main은 비용이 많이 들 수 있으므로
Update 메서드에서는 호출하지 말고, Start에서 호출하고 결과를 캐시하자.

오브젝트 풀 사용

Instantiate 함수 및 Destroy 함수는 가비지와 가비지 컬렉션 스파이크를 야기하며,
일반적으로 속도가 느린 프로세스이므로, 계속 인스턴스화하고 삭제해야하는 경우가 많다면,
오브젝트 풀을 사용하자.

프로젝트에서 관리되는 할당의 수가 줄어들기 때문에 가비지 컬렉션 문제를 방지할 수 있다.
유니티 공식 튜토리얼 - 오브젝트 풀링

스크립터블 오브젝트 사용

변하지 않는 값 또는 설정은 MonoBehaviour가 아닌 스크립터블 오브젝트에 저장하자.

스크립터블 오브젝트는 한 번만 설정하면 되는 프로젝트 내부의 에셋으로
게임 오브젝트에 직접 연결할 수 없고,
스크립터블 오브젝트에서 필드를 생성하여 값을 저장한 다음,
MonoBehaviours에서 스크립터블 오브젝트를 참조하면 된다.
스크립터블 오브젝트 소개영상
COMMENT