메모리 관리에 대한 탐구 (5) - 메모리 프로파일러를 통한 메모리 사용량 분석
오늘은 유니티에서 Package Manager를 통해서 제공하는 Memory Profiler에 대해서 소개할 예정이다. 유니티 2021에서 제공하는 0.7.1 버전을 기준으로 소개를 할 것인데, 현재는 1.0도 나와있다. UI나 세부적인 부분은 바뀌었겠지만 아마 핵심적인 부분들은 크게 바뀌지 않았을 것이라고 생각된다.
1.0에 대한 내용은 Unity Korea 채널에서 오지현님의 Memory Profiler 1.0 소개 영상을 보면 도움이 될 것 같다.
우선, 기본적으로 제공하는 프로파일러에서도 메모리에 대한 대략적인 정보와 어느정도의 디테일한 정보를 볼 수 있었지만, 지금은 Detail한 정보를 보려면 메모리 프로파일러를 통해서 보라고 한다. 메모리 프로파일러는 Heap Memory에 올라와있는 GameObject, Transform은 물론이고 Texture2D, Shader 등 Native Memory에 있는 리소스들까지 전부 볼 수 있다. 프로파일러처럼 실시간으로 보지는 못하지만, 원하는 시점에 스냅샷을 찍어서 해당 시점의 메모리 상황을 저장해놓고, 다른 스냅샷과 비교하면서 어떤 차이가 있는지 살펴볼 수 있는 훌륭한 도구이다.
메모리 프로파일러를 열면 다음과 같은 창을 볼 수 있다. 처음에는 아무것도 없는 상태이다. 기본적인 정보를 설명하자면 이렇다.
1. Editor라고 되어있는 부분을 누르면 Editor, 혹은 모바일 기기 등을 선택하고 스냅샷을 찍을 수 있다. 모바일 기기를 연결하기 위해서는 안드로이드는 ADB 관련 세팅이, ios는 XCode 관련 세팅이 필요할 것이다. Editor에서는 유니티 Editor에서 사용되는 메모리들도 같이 잡히기 때문에 실제 타겟 기기에서 사용하는 메모리와는 상당히 차이가 크게 나타나기 때문에, 의미있는 메모리 분석을 하기 위해서는 타겟 기기에서 스냅샷을 찍는 것이 좋다.
2. 스냅샷을 찍는 버튼이다. 오른쪽 버튼을 누르면 스냅샷을 찍을 때 기록할 메모리들과 일부 설정들을 조절할 수 있다. 스냅샷을 찍을 때 경고창이 나타나는데, 모든 메모리들이 기록되기 때문에 중요한 정보들이 유출되지 않도록 조심하라는 내용이다.
3. Single Snapshot은 1개의 스냅샷에 대한 자세한 정보를 볼 수 있고, Compare Snapshots를 누르면 2개의 스냅샷을 선택해서 각 상황에서의 메모리 변화를 비교할 수 있다.
4. 기록한 스냅샷이 저장된 모습이다. 기록할 당시의 스크린샷과 이름, 기록한 시간 등이 저장된다. 스크린샷의 오른쪽 아래에는 어느 플랫폼에서 찍었는지 나타내준다. 위 사진의 경우 윈도우, 유니티 에디터에서 찍었기 때문에 두 아이콘이 나타났는데, 안드로이드 기기에서 찍으면 안드로이드 모양 아이콘이 나타나는 식이다. 이름을 누르면 이름 변경이 가능하고, 그 외의 부분을 누르면 스냅샷을 살펴볼 수 있다. 우클릭으로 추가 메뉴를 열 수도 있다. 정보를 세세하게 기록하기 때문에 용량이 상당히 큰데, 대략 200~300MB정도 된다.
스냅샷을 누르면 기록할 당시의 메모리의 세부적인 부분을 볼 수 있다. 테스트용 안드로이드 기기를 회사에 놓고 와서 에디터에서 기록한 스냅샷을 기준으로 설명하겠지만, 정확한 분석을 위해서는 타겟 기기에서 스냅샷을 기록할 필요가 있다는 점을 다시 한 번 강조하겠다.
Managed Heap은 가비지 컬렉터가 관리하는 메모리 공간으로, 우리가 객체를 생성하는 등의 활동을 할 때 이 영역을 차지한다. 96.3MB를 할당받은 상태이기 때문에 사용량에 관계없이 96.3MB의 메모리 공간을 차지한다. 현재 44MB밖에 사용하고 있지 않은데, 이 상태가 지속되면 자동으로 할당받은 메모리 공간이 줄어든다.
Vitrual Machine (Mono)는 에디터에서 실행하거나, Mono로 빌드한 경우에 사용되는 메모리이다. 유니티 엔진은 C++로 되어있기 때문에 프로그래머가 작성한 C# 코드를 그때그때 C++ 코드로 변환하면서 사용되는 메모리이다. IL2CPP 방식으로 빌드하면 해당 메모리 사용량이 0이 되니 크게 신경쓸 필요는 없다.
이외에는 Native Memory나 프로파일러, 유니티에서 사용하는 메모리 등을 보여주는데, 에디터라 전체적인 사용량이 큰 모습이다.
현재 화면에서는 대략적인 사용량만 볼 수 있는데, 실제로 자세히 분석을 위해서는 상단에 있는 메뉴들을 클릭해주어야 한다.
https://docs.unity3d.com/Packages/com.unity.memoryprofiler@0.7/manual/memory-breakdowns.html
Memory Breakdowns | Memory Profiler | 0.7.1-preview.1
Memory Breakdowns The Memory Breakdowns tab displays a broader view of how the memory in your snapshot is distributed. Use this information to identify areas where you can eliminate duplicate memory entries or to find which objects use the most memory. The
docs.unity3d.com
Memory Breakdowns는 2022.1 이상의 버전에서 지원하는 기능이다. 여기서는 자동으로 Type별로 묶고, 크기를 내림차순으로 정렬하여 보여주는 것 같다. 유니티에서 분석하여 Potential Duplicates view라는 것도 보여주어 메모리를 조금 더 간편하게 분석할 수 있도록 지원하는 것 같다. 나는 아직 2021 버전에서밖에 사용해보지 않았기 때문에 더 자세한 내용은 위 문서를 참조하길 바란다.
Tree Map을 누르면 현재 전체 메모리 사용량을 Tree Map 형식으로 볼 수 있다. 현재 왼쪽 위 사각형인 Texture2D를 눌러서 Texture2D에 대한 자세한 정보가 표시된 모습이다. 현재 어떤 요소가 메모리를 많이 잡아먹고 있는지, 그 안에는 어떤 것들의 사용량이 큰지 눈으로 쉽게 파악할 수 있다.
그 다음인 Objects And Allocations는 내가 가장 자주 사용하는 탭으로, 전체 메모리를 한 눈에 살펴볼 수 있다.
위에 Data Type, Type, Name, Size 등의 부분을 누르면 해당 내용을 기준으로 Group을 짓거나, 원하는 내용만 표시할 수도 있으며, 크기순으로 정렬할 수도 있다.
예를 들어 Type으로 Group짓고, Size를 내림차순으로 정렬하면 이런 식으로 메모리를 많이 차지하고 있는 항목들을 순서대로 볼 수 있다.
원하는 내용을 펼쳐보면 내부에서도 내림차순으로 정렬되어 있는데, 기능 구현에 중점을 둔 포트폴리오라 메모리 최적화는 신경을 쓰지 않아서 오디오 클립 하나가 20.6MB를 차지하고, Texture 사이즈도 제멋대로라 압축이 제대로 되지 않은 등의 모습을 확인할 수 있었다. 만약 메모리가 부족하다면 이곳에서 큰 용량을 차지하는 메모리를 줄이는 등의 목표를 세울 수 있다.
Compare Snapshots로 2개의 스냅샷을 비교해보면 이런 식으로 나타난다. 게임을 켜고 아무것도 하지 않았음에도 불구하고 전체적인 메모리 사용량이 크게 늘어난 모습이다. 에디터이기 때문에 그렇다. 스냅샷을 찍을 때마다 메모리가 아주 큰 폭으로 늘어나기 때문에 시간이 지날수록 렉이 걸리는 것을 경험할 수 있을 것이다. 모바일 기기에서 테스트를 하는 경우, 특수한 경우가 아니라면 한참이 지난 후에 찍더라도 전체 용량이 1MB도 채 변하지 않는다. 그래서 정확한 분석을 위해서는 타겟 기기에서 스냅샷을 찍으라고 누차 이야기를 하고 있는 것이다.
Compare Snapshots에서는 Tree Map은 제공하지 않고, Objects and Allocations를 이용해야 한다.
다시 Objects and Allocations로 들어와보면 Diff라는 탭이 하나 더 생긴 것을 확인할 수 있다.
또, 상단의 메모리 사용량을 보면 Total Sizes: 뒤에서 두 스냅샷에서의 메모리 사용량의 차이를 볼 수 있다.
Diff를 기준으로 Group지으면 이렇게 Same, Deleted in B, New in B를 나눠서 확인할 수 있다. Deleted in B와 New in B를 중점적으로 살펴보면서 삭제되서는 안 될 것들이 삭제되었다든지, 생성되서는 안 될 것들이 생성되었다든지를 확인하면 된다. 위의 경우, 가만히 있었는데도 105MB가 삭제되고 284.5MB가 생성되었다는 것은 무언가 큰 문제가 있다는 뜻이다. 물론 에디터에서 찍은 스냅샷이라 그럴 것이다.
아까와 같이 Type별로 추가로 Group짓고, Size를 내림차순으로 정렬해보면 더 쉽게 파악할 수 있다. 위 사진에서 볼 수 있듯이 RenderTexture같은 경우는 똑같은 것들이 삭제되었다가 새로 생긴 것을 볼 수 있는데, 모종의 이유로 파괴되었다가 다시 생성되어 교체된 것이다. 음... 이 경우는 이유는 잘 모르겠다. 아마 화면이 완전히 가려져서 그런 것이 아닐까 생각해본다.
실질적으로 어떻게 유용하게 사용할 수 있는지 예시를 들어보자면, 나의 경우에는 비교 작업을 통해서 몬스터를 잡을 때 몬스터의 Material, SkinnedMeshRenderer 등이 매 번 삭제되고 생성되고 있는 것을 확인할 수 있었다. 무언가 이상하다고 느껴 확인해보니 몬스터에 대한 오브젝트 풀링이 제대로 작동하지 않고 있었던 것을 발견하고 수정하였다. 이를 통해서 사냥 중 가비지 컬렉터가 작동하는 주기를 50% ~ 100% 혹은 그 이상 늘릴 수 있었다. (평소에는 2분 주기로 GC가 호출되었는데, 3~4분 정도로 바뀌었고, 몬스터가 위치하는 필드가 작은 등의 일부 경우에는 30분 가량 호출이 되지 않을 때도 있었다)
또, AssetBundle 사용 관련 문제로 인해 Shader가 복제된 것을 확인하였는데, 에셋 번들 빌드 중 Dependencies가 제대로 등록되지 않아 해당 원인이 무엇인지 파악 중에 있다.
추가로, Shader 용량이 예상보다 큰 것을 확인하여 사용하지 않는 Varient를 없애 전체 Shader 용량을 1/4 가량으로 줄이는 작업에 조금이나마 기여를 한 바가 있다.
그리고 풀링한 오브젝트들 중에 사용한 지 오래된 리소스들을 정리하는 작업도 하였는데, 이 과정에서 불필요하게 메모리를 차지하고 있는 리소스(AudioClip 등)를 발견하여 정리해주는 작업을 추가로 하기도 하였다.
이외에도 따로 한참 방치해두며 스냅샷을 찍어 메모리 누수가 없는 것을 확인하기도 하였다.
여태까지 약 2~3주간 사용법을 익히고 위에 언급한 것처럼 내가 생각해 낸 방식대로 사용을 하였는데, 더 유용한 사용법들이 많이 있을 것 같다. 위에 언급한 내용들만으로도 충분히 사용할 가치가 있을 것이라고도 생각한다. 메모리 관리에 어려움이 있다면, 메모리 프로파일러를 사용해보는 것을 적극 권장하는 바이다.
마지막으로, ADB의 dumpsys meminfo를 활용하여 기기 내의 전체 용량과 어플리케이션이 사용하는 비중을 확인하는 것도 도움이 될 수 있을 것 같다.