▲ 유니티테크놀로지스코리아 오지현 서포트엔지니어

[인벤게임컨퍼런스(IGC) 발표자 소개] 10년이 넘는 경력의 엔지니어. 레이더즈와 건즈2의 엔진 개발에 참여했으며, 현재는 유니티테크놀로지스코리아에서 서포트엔지니어로 근무중이다.

최적화는 개발자에게도 게이머에게도 중요한 '덕목'이다. 물론 게임에서 가장 중요한 것은 '재미'다. 그러나 게임을 아무리 재미있게 만들어도 쾌적한 성능이 나오지 않는다면 게이머들로부터 외면받을 수밖에 없다. 그러므로 최적화는 개발 과정에 있어 매우 중요한 부분이다.

국내 모바일 게임 개발 씬에서 많이 사용되고 있는 '유니티 엔진'은 손쉽게 병목구간을 파악하고 이를 해결할 수 있는 툴을 제공한다. 유니티테크놀로지스코리아의 오지현 서포트엔지니어는 게임을 최적화하기 위해 왜 성능을 프로파일링하는 것이 중요한지 청중들에게 가이드를 제시하고 툴 사용 방법을 공유했다.



■ 강연주제: “뭣이 중헌디? 성능 프로파일링도 모름서” – 유니티 성능 프로파일링 가이드

⊙ Pixel Shader? Texture Size? Physics? Draw Call? Memory?

성능을 최적화하는 과정에서 중요한 것은 병목구간을 파악하고 이를 효과적으로 수정하는 것이다. 무조건 드로콜(Draw Call)을 줄인다든지, 단순히 메모리가 문제가 되니까 텍스쳐 사이즈(Texture Size)를 줄인다든지 하는 것은 별 도움이 되지 않는다. 진짜 문제가 무엇인지 모르기 때문이다.

최적화할 때는 무엇이 중요한지 판단하고 해야 한다. 무작정 직관으로 작업을 해 나가는 것은 의미가 없다. 1인 개발이 아닌 이상, 각 부문의 작업물이 합쳐지는 게임은 수치화된 데이터를 뽑아내고 이를 분석하는 것이 필수적이다.

어디에서 병목이 발생하는지 정확한 측정을 통해 찾아내야 한다. 그러므로 프로파일링은 매우 중요하다. 하나로 합쳐진 결과물이 어떻게 나올지는 아무도 모른다. 프로파일링하기 전까지는 말이다.

▲ 프로파일링의 목적

많은 개발팀이 간과하고 있는 것이 프로파일링을 개발 막바지에 한다는 것이다. 예를 들어 출시 3주 전에 성능 프로파일링을 하는 팀이 있다고 해보자. 이 팀은 병목 구간을 발견하고 온갖 수단을 동원해 수정 작업을 한다. 그래도 출시 전까지 제대로 된 수정을 하지 못할 확률이 높다. 만약 폴리곤이 병목이어서 이를 반 토막 내야 하는 상황이라면 물리적으로 가능한 시간일까? 쉐이더도 마찬가지다. 바꿀 시간이 부족하다.

그렇다면 게임 초기에 하는 것이 좋은가 하면 그것도 아니다. 개발 초반부에는 프로토타이핑하면서 게임의 기능을 만들고 검증하고 또 만들고 검증하는 작업을 빨리 해야 하는 데 이 부분에서 성능을 측정하면 효율적인 개발이 어렵다. 어차피 초반부 코드는 나중에 버려지기 마련이다.

그러므로 최적화 프로파일링은 개발 중반부부터 주기적으로 점검하면서 개발을 진행하는 것이 효과적이다. 개발 중반부는 어느 정도 핵심 피쳐가 만들어진 상황에서 양적으로 규모를 키워가는 시기를 말한다.


⊙ 유니티 내장 프로파일러(Profiler)


프로파일링은 적당한 툴의 도움이 없다면 빠르게 진행하기가 매우 어렵다. 얻은 데이터를 어떻게 잘 활용하느냐가 제일 중요하지만, 성능을 측정하고 어느 부분에서 병목이 발생하는지 파악하는 것은 툴의 도움을 받아야 하기 때문이다.

유니티 엔진은 기본적으로 프로파일러를 제공하고 있다. 이 툴은 게임의 다양한 부부들에 대해 얼마나 많은 시간을 소요하고 있는지를 알려준다. 예를 들면, 게임 로직 내에서 렌더링이 차지하는 시간의 비율 그리고 애니메이션에 소요된 시간이 얼마인지 등을 알려준다.

사실 에디터 상에서 프로파일링하는 것은 큰 의미가 없다. 기기마다, 플랫폼마다 병목 지점이 다르기 때문이다. 그러므로 타겟 기기를 직접 연결해 측정해서 결괏값을 얻어야 한다.

▲ 타임라인을 통해 직관적으로 파악할 수 있다.

프로파일러 창을 통해서 타임라인 내에 해당 데이터를 확인할 수 있어, 다른 것들에 비해 더 많은 시간을 소모하는 영역 또는 프레임을 확인할 수 있다. 타임라인 위에 임의의 위치를 선택하면 프로파일러 창 아래쪽에 선택된 프레임에 대한 자세한 정보를 얻을 수 있다.

유니티 프로파일러를 사용하면 직관적으로 데이터를 확인할 수 있다. 또한, 메인 스레드뿐만 아니라 워커 스레드에서 어떻게 하고 있는지까지 타임라인을 통해 알 수 있다.


위 예시는 디바이스 프레젠트(Device.Present)가 53%를 점유하고 있는 상태를 보여준다. 이를 해결하기 위해서는 CPU와 GPU가 일하는 방식에 대해 알 필요가 있다. CPU와 GPU는 병렬 관계로 작업한다. 프레임이 시작되면 CPU는 자신의 작업을 하다가 GPU에 명령을 내린다. 그리고 다시 CPU는 자신의 작업을 계속한다. GPU는 CPU에게 명령을 받는 순서대로 이를 수행한다. 그렇게 CPU가 제 일을 끝내고 화면을 출력하려고 할 때 GPU의 작업이 끝나지 않았다면 CPU는 GPU의 작업이 끝날 때까지 기다린다.


즉 이 부분은 CPU가 기다리고 있는 상태를 의미한다. 그러므로 디바이스 프레젠트가 높을수록 GPU의 부담이 높다는 이야기고 이 부담을 줄여주는 게 성능을 높이는 방법이다.

▲ 타겟 기기인 LG G5에서 6 FPS.

또 다른 예시다. QHD 해상도에 후처리도 사용하고 있고 파티클도 많아 보인다. 1초에 6프레임 밖에 나오지 않는 상황이다. 언뜻 보기에는 GPU 병목으로 판단할 수 있지만, 실제로 프로파일링을 해보면 렌더포워드,렌더루프잡(RenderForward,RenderLoopJob)에 문제가 있음을 파악할 수 있다.

실제로 렌더링 루프를 돌리면서 드로콜을 날려주는 게 대부분이라는 뜻이다. 즉 병목은 드로콜이다. 화면만 보면 GPU 병목일 것 같지만, 실제는 드로콜 병목임을 알 수 있다. 그러므로 측정할 때는 반드시 타겟 기기에 측정을 해야 한다.


⊙ 프레임 디버거(Frame Debugger)

'유니티5'부터 프레임 디버거(Frame Debugger)가 추가되어서 프레임별 렌더링 과정을 디버깅해볼 수 있다. 개발자는 이를 통해 오브젝트의 렌더링 과정이나 배칭 현황을 손쉽게 확인할 수 있다.

프레임 디버거는 에디터에서 봐도 무방하며 어느 쉐이더 프로퍼티가 드로우콜에 의해 사용되는지를 확인해 볼 수 있다. 또한, 이미지 후처리 진행 과정이나 UI가 어떤 순서로 어떻게 그려지는지도 확인할 수 있다.

▲ 프레임 디버거 창.

예시로 든 것은 라이트 맵 해상도를 적게 잡아 배칭 콜이 늘어나는 사례다. 라이트 맵을 많이 사용하면 사용할수록 배칭이 깨질 확률이 높아진다. 라이트 맵 역시 드로콜에 영향을 준다. 이는 픽셀 사이즈를 수정하고 라이트맵을 하나로 만들어서 해결할 수 있다. 이처럼 프레임 디버거를 활용하면 어떻게 드로콜이 이뤄지고 어떻게 배칭되었는지를 직관적으로 확인할 수 있다.

▲ 라이트맵을 줄였다.

비단 3D 게임 뿐만 아니라 2D 게임에도 프레임 디버거를 활용할 수 있다. 예시로 든 파일은 드로콜 좀 많은 사례다. 일일이 순차적으로 모든 오브젝트와 캐릭터를 불러오고 있는 형태다. 아틀라스(Atlas) 처리가 되지 않은 상태라 다른 오브젝트를 그리다가 배칭이 깨질 확률이 높다.

이 사례의 경우 리소스 작업 효율 때문에 그림자를 별도의 런타임으로 생성하면서 붙여주고 있다. 그림자가 치고 들어오니까 배칭이 깨질 수밖에 없다. 캐릭터를 그리고, 체력바를 그리고, 그림자를 그리는 과정을 계속해 하고 있어서 드로콜 개수도 많고 자연히 성능도 나빠졌다.

▲ 2D에서도 디버거를 활용할 수 있다.

스크린에 오브젝트를 렌더링하기 위해 렌더링 엔진은 OpenGL이나 Direct3D 그래픽 API에 드로콜을 해야 한다. 그래픽 API 드로콜은 CPU 상의 상당한 퍼포먼스 오버헤드를 일으킨다. 그러므로 드로콜을 정리할 필요가 있다.

이는 소팅 레이어(Sorting Layer)를 지정하는 것만으로도 해결할 수 있다. 그림자, 오브젝트, UI로 분류해 관련 있는 것끼리 묶어 배칭이 깨지지 않게 할 수 있다. 이런 식으로 160개에 이르는 드로콜을 25개로 줄일 수 있다. 코드 한 줄 넣지 않고 설정만 바꿔서 말이다. 2D 역시 프레임 디버거를 활용하면 드로콜을 확인하고 줄이는 데 도움을 줄 수 있다.

▲ 확 줄어든 드로콜




⊙ 메모리 프로파일러(Memory Profiler)

모바일 게임의 경우 PC 게임보다 메모리에 더 많은 신경을 써야 한다. PC의 경우 디스크 일부를 이용해 메모리 사용을 보완할 수 있지만, 모바일은 이런 기능이 없기 때문이다. 실제 물리 메모리를 벗어나는 내용을 사용할 수 없다. 그래서 메모리를 많이 사용하면 OS가 해당 앱을 강제적으로 종료하는 경우가 허다하다.

유니티 5.3부터 메모리 프로파일러를 이용할 수 있다. 메모리 프로파일러는 유니티에 내장되어 있지는 않지만, bitbucket을 통해 유니티 엔진에 붙일 수 있다.


다운로드 받은 프로젝트의 에디터 폴더를 작업 중인 프로젝트의 에디터 폴더로 드래그앤드랍 후 이름을 변경한다. 스크립트 백엔드는 IL2CPP로 안드로이드는 유니티 5.4부터 지원한다. 메모리 프로파일러 툴은 기본적으로 유니티 프로파일러 API를 사용하기 때문에 디벨롭먼트 빌드로 실행해야 하며 유니티 프로파일러 윈도우를 실행하고 디바이스를 연결한 후 사용할 수 있다.

▲ 시각적으로 확인할 수 있다.

메모리 프로파일러는 수집한 데이터를 시각적으로 보여준다. 우측에는 텍스쳐 이름이 나오고 어떤 인스턴스(instance)를 사용하는지 레퍼런스 정보를 볼 수 있다. 또한, 메모리 사용량이 어떻게 나오는지 자세히 볼 수 있다.


Android의 압축(Compressed)은 RGB 텍스쳐의 경우 ETC1로, RGBA 텍스쳐의 경우 ETC2 인데 ETC2는 OpenGL ES3.0부터 지원한다. OpenGL ES2.0에서 ETC2를 사용하면 압축이 풀려서 메모리에 적재되므로 주의를 요한다. 상기 메모리는 ES3.0을 지원하는 상황에서는 4MB까지 떨어진다.

▲ 게임을 개발하면서 텍스쳐가 계속 추가되는데 에셋 스크립터를 사용해 메모리를 절약할 수 있다.


▲ 오디오가 차지하는 비율도 확인할 수 있다.

예시 화면은 오디오 클립하나가 4.6MB를 차지하는 화면이다. 로드 타입을 보면 Decompress On Load로 선택되어 있다. 이는 오디오 압축을 풀어서 메모리에 적재하는 방식으로 BGM처럼 큰 오디오는 성능에 치명적인 영향을 주므로 작은 효과음만 사용해야 한다.

iOS에서 Vorbis를 사용해도 마찬가지다. iOS에서는 OGG 네이티브를 지원하지 않는다. 그래서 유니티가 메모리를 올릴 때 압축을 풀어서 올리므로 갑자기 메모리 사용량이 늘어난다. 그러므로 iOS에서는 MP3 사용이 권장된다.

폰트도 메모리 최적화에서 빼놓을 수 없다. 예시 화면은 똑같은 나눔 고딕 폰트인데 인스턴스 ID가 다른 경우를 보여주고 있다. 개발 과정에서 실수로 어쩌다 보니 한 씬에 두 개의 폰트를 올리는 경우가 있다. 이런 경우는 충분히 일어나는 경우로 비단 폰트뿐만 아니라 텍스쳐, 리소스 모두 해당하는 이야기다.

중복 리소스에 있어 유니티는 실제로 같은 파일인지 체크하지 않는다. 이러한 실수들은 메모리 프로파일러로 확인할 수 있다.

▲ 같은 폰트 다른 인스턴스 ID.



⊙ 엑스코드 인스트루먼트(Xcode Instrument )


외부툴을 사용하면 성능을 더욱 효과적으로 관리할 수 있다. 특히 엑스코드(XCode)는 훌륭한 프로파일러다. 아이폰, 아이패드 개발에 있어 엑스코드는 많은 도움을 줄 수 있다. CPU뿐만 아니라 GPU, 에너지 효율, 메모리 렉, 디스크IO 등 여러 가지를 확인할 수 있다.

CPU 사용량은 인스트루먼트(Instrument)로 확인할 수 있다. CPU 프로파일링은 인터벌을 줘서 프로세스들이 동작하는 샘플을 얻어냄으로써 측정한다. 스냅샷들 사이에서 어떤 프로세스들이 아직 돌고 있는지 봄으로써, 그것들이 얼마나 길게 작동되고 있는지 측정할 수 있다. 이를 통해 직관적으로 확인하며 앱의 정확한 지점을 최적화할 수 있다. 한 프레임만 선택해 보는 게 아니라 드래그를 해 영역을 볼 수도 있다


예를 들어 상기와 같이 단 한 줄이 5%를 차지하는 경우도 발견할 수 있다. 셀프 파라미터(Self.Parameters)는 애니메이터에서 파라미터 목록을 얻어오는 부분인데 단 한 줄이지만 유니티 내부적으로는 많은 일을 한다. 호출할 때마다 목록을 생성하는 등 많은 일을 수행하는데 이러한 호출이 프레임마다 일어나기에 많은 비율을 차지하는 것이다.

이처럼 엑스코드를 활용하면 유니티 내장 프로파일러로는 획득할 수 없는 데이터를 살펴볼 수 있다. 그러므로 아이폰 개발을 한다면 엑스코드는 필수적으로 확인하는 것이 좋다.


GPU 역시 프로파일링을 할 수 있다. GPU 프로파일링할 때는 반드시 프레임 캡쳐를 메탈(Metal)로 설정해야 한다. 화면에 나타나는 타일러(TILER)은 버텍스(Vertex)를 뜻하며 렌더러(Renderer)는 픽셀(Pixel)처리를 뜻한다. 즉 타일러의 그래프가 높으면 버텍스를, 렌더러 그래프가 높으면 픽셀 쉐이더 해상도를 줄여줘야 한다. 프레임 란에서는 CPU에 집중되어 있는지 GPU에 집중되어 있는지 프레임마다 통계를 내서 볼 수 있다.

또한, 드로콜 별로 확인할 수 있으며 실제 이벤트도 볼 수 있다. 엑스코드는 유니티의 프레임 디버거보다 더 많은 정보를 제공한다. 드로콜 시점에서 파이프라인 퍼포먼스가 어떻게 나오는지 알 수 있으며 더욱 자세한 정보, 이를테면 어떤 쉐이더를 사용하는지, 현재 시점에서의 알파 상태라든지, 렌더링 상태를 확인할 수 있다.


위와 같이 유니티 쉐이더를 메탈 쉐이더로 변환한 부분에서 어떤 부분이 많은 비율을 차지하고 있는지 옆에 출력되는 표시로 쉽게 알 수 있다. 모바일 같은 경우 쉐이더에서 비교문을 쓰면 성능에 치명적인 결과를 초래한다.

예시 화면을 통해 Diffuse가 무겁다는 것을 알 수 있다. 일반적으로 많이 사용하는 것이지만, 밑을 보면 그림자맵을 샘플링해와서 사용하는 부분이라 점유율이 높은 것임을 발견할 수 있다. 즉 씬 렌더링에서 그림자 처리하는 게 병목임을 알 수 있다. 이처럼 엑스코드의 기능을 사용하면 현재 프레임에서 어떤 부분이 병목인지 확인할 수 있다.


⊙ 쓰로틀링(Throtlling)

기기는 연산을 할수록 뜨거워진다. 계속 뜨거워지는 것을 방지하기 위해 연산 속도를 낮춰서 발열을 막는 기능을 쓰로틀링(Throtlling)이라고 한다. 사실 이 기능은 사용자가 게임을 할 때는 매우 필요한 기능이지만, 프로파일링할 때는 문제가 되기도 한다. 데이터를 어지럽히는 주범이기 때문이다.

프로파일링은 측정하고 수정하고 측정하고 수정하는 작업을 반복하는 것인데, 쓰로틀링 때문에 늦어지는 건지 아니면 잘못 수정해 느려진 건지 파악하기 쉽지 않다.

일부 기기는 쓰로틀링 기능을 끌 수 있지만, 그렇지 않은 기기도 존재한다. 이럴 경우 스마트폰용 쿨러를 사용한다든지, 아이스팩을 활용해 쓰로틀링을 끄고 측정해야 한다. 외부요인에 의해 뜻하지 않게 데이터가 어지럽혀지는 것을 항상 염두에 둬야 한다.

프로파일링 데이터는 여러 방법을 통해 획득할 수 있다. 그러나 아무리 많은 데이터를 모아도 제대로 분석을 하지 못한다면 데이터는 쓸모없는 숫자에 불과하다. 혼자서 해결하기 어려울 때는 유니티를 잘 이해하고 있는 전문가의 도움을 받을 수도 있다. 이를 위해 유니티에서는 프리미엄 기술 지원 서비스를 제공하고 있다.