본문 바로가기

Unity Engine/자료 번역

Memory Management in Unity 유니티 메모리 관리 번역

728x90
728x90

이 포스트는 유니티 튜토리얼을 번역한 것이다. 매끄럽지 않은 번역이나 오역이 있을 수 있음에 유의하고 충분한 영어 독해가 가능하면 원본을 보면 좋다.

 

여담이지만 원문에도 어색한 맥락이 있는데 이 포스트에도 그런 느낌이 있다면 그것은 그 맥락을 그대로 옮겨서 그런 것이니 이를 참고하면 좋겠다. 만약 오류가 명백히 보인다면 제보해주면 감사하게 반영하겠다.

 

참고: 2019년 3월 6일 자 버전을 번역했다.

요약

유니티 엔진에서 적절한 메모리 관리는 도전적인 일이 될 수 있다.

 

이 가이드에서는 공개적으로 사용 가능한 플랫폼(publicly available platform)에서 메모리 사용을 프로파일, 최적화하는 필수 지식을 습득하는 것을 목표로 한다.

1. A guide to optimizing Memory

유니티에서 적절한 메모리 관리는 도전적인 일이 될 수 있다.

 

다음 요소를 고려해야 한다.

 

1. 자동 메모리 관리의 뉘앙스

2. native와 managed 메모리의 차이

3. 메모리가 다른 플랫폼에서 어떻게 동작하는지

 

이 가이드에서는 공개적으로 사용 가능한 플랫폼(publicly available platform)에서 메모리 사용을 프로파일, 최적화하는 필수 지식을 습득하는 것을 목표로 한다.

2. Managed Memory

Managed heap을 이해하는 것은 유니티 메모리 관리에서 필수이다.

 

Managed 메모리 프로파일링과 메모리 최적화에 추가 정보가 필요하면 다음 링크를 참조한다.

 

Understanding managed memory section

Understanding Optimization in Unity guide

에셋

에셋은 런타임에 native 메모리와 managed memory implication을 유발할 수 있다.

 

유니티 런타임은 유저가 애플리케이션을 종료하지 않으면 managed 메모리를 OS에 반환하지 않는다.

 

Managed heap은 메모리 파편화가 심해져서 사용 가능한 메모리가 부족해지면 더 커진다.

 

이런 예측 불가 동작으로 인해 에셋이 managed 메모리를 어떻게 차지하는지 파악하는 것은 중요하다.

  • Destroy() 함수를 호출하여 오브젝트를 제거하고 메모리를 릴리즈 한다. 그러면 해당 오브젝트의 레퍼런스는 null이 된다. 오브젝트 자체는 삭제되지 않는다.
  • Persistent(수명이 긴) 오브젝트는 클래스로 만들고 ephemeral(수명이 짧은) 오브젝트는 스트럭트로 만들어야 한다. 스트럭트는 GC에 의해 수집되지 않기 때문에 힙에 할당되지 않는다.
  • 임시 쓰레기(temporary garbage)를 낮게 유지하기 위해 빈번한 할당을 하는 대신 *temporary work buffers를 재사용한다.
  • Enumerator는 종료 전까지 메모리를 정리하지 않는다.
  • Never-ending 코루틴을 피해라. **코루틴은 스택이 아닌 힙에 메모리를 할당하므로 많은 양의 메모리를 사용할 때면 never-ending 상태가 되지 않도록 한다.

*역주: temporary work buffers는 이미 존재하는 변수를 의미하는 것으로 보인다. 잘못된 해석이면 댓글에 제보를 부탁한다.

**코루틴 최적화에 관해서는 여기를 참고하면 좋을 것이다.

3. IL2CPP & MONO

iOS와 안드로이드에 대해, Player Settings에서 스크립팅 백엔드로 Mono 혹은 IL2CPP를 선택할 수 있다. 스크립팅 백엔드를 바꾸려면 Edit > Project Settings > Player > Other Settings 순으로 들어가 바꾸면 된다.

 

참고(번역하지 않음): As of 2017.3, choose either the IL2CPP Scripting Backend or the Mono Scripting Backend. However, both WebGL and UWP only support IL2CPP. iOS still supports the Mono Scripting Backend for fast iteration, but you cannot submit Mono (32-bit) application to Apple anymore.

서로 다른 스크립팅 백엔드의 장단점

각 스크립팅 백엔드가 가진 장단점은 상황에 따라 결정에 영향을 미친다.

 

IL2CPP

  • Mono에 비해 코드 생성이 막대하게 향상됨
  • 스크립트 코드는 C++로 탑 다운 디버깅이 가능함
  • 코드 크기를 줄이기 위해 Engine code stripping을 활성화할 수 있음
  • Mono보다 빌드 시간이 오래 걸림

Mono

  • IL2CPP보다 빌드 시간이 빠름
  • JIT로 인한 더 많은 managed 라이브러리 지원
  • 런타임 코드 실행 지원
  • Managed assemblies를 필수로 ship 해야 함. 여기서 말하는 어셈블리는 모노 혹은 닷넷이 제공하는 dll 파일

팁: 개발과 ship 둘 다 하는 프로젝트는 IL2CPP를 사용하는 게 좋다. IL2CPP를 사용하는 동안 *iteration time이 너무 길어지면 잠시 Mono로 바꿔 iteration time을 향상할 수 있다.

 

*여기서 의미하는 iteration time이 뭔지 제보를 부탁한다. 추가로 ship도

 

참고: Player Settings에서 기본 대상 아키텍처(Target Architecture)는 릴리즈 빌드에 최적화되어있다. 개발하는 동안 기본값으로 빌드를 할 때 빌드 시간이 늘어나는 이유는 각 대상 아키텍처에 대해 바이너리 파일을 작성하기 때문이다.

  • 안드로이드 Player Settings에서 기본 대상 아키텍처는 armv7, x86이고 스크립팅 백엔드로 Mono를 쓴다.
  • iOS Player Settings에서 기본 아키텍처는 armv7, amr64이고 스크립팅 백엔드로 IL2CPP를 쓴다.

Code stripping in Unity

코드의 크기는 디스크 크기와 런타임 메모리에 직접 영향을 끼친다. 유니티가 코드 베이스에서 사용하지 않는 code paths를 제거하는 것은 중요하다.

 

유니티는 빌드하는 동안 두 가지 다른 레벨에서 코드 제거를 수행한다.

 

1. Managed code stripping

2. Native code stripping

Managed code stripping

유니티는 메서드 레벨에서 managed code를 제거한다. Stripping 레벨을 조정하려면 Player Settings > Other Settings > Stripping Level을 찾은 뒤 드랍 다운 메뉴에서 Strip Assemblies를 선택하면 된다.

 

UnityLinker는 CIL로부터 사용하지 않는 타입들(클래스, 스트럭트 등)을 제거한다. 사용하는 타입이어도 UnityLinker는 해당 타입에서 사용하지 않는 메서드를 삭제한다.

 

참고: 이 기능은 Mono에서는 옵션이지만 IL2CPP에서는 항상 활성화된다.

Native code stripping

유니티는 Strip Engine Code가 Player Settings에서 기본으로 활성화, 그리고 native code stripping이 활성화되어있다. Strip Engine Code를 활성화하여 네이티브 유니티 엔진 코드에서 사용하지 않는 모듈과 클래스를 제거한다. Strip Engine Code를 비활성화하면 엔진의 모든 모듈과 클래스를 보존하게 된다.

Unity Module Stripping

참고: WebGL은 현재 쓰지 않는 모듈만 제거하는 유일한 플랫폼이다.

 

유니티는 사용하지 않는 유니티 모듈을 제거하기 위해 최선의 시도를 한다. 이는 어떤 씬이나 스크립트가 빌드에 포함된 유니티 모듈의 컴포넌트를 참조하는 경우, 삭제하지 않음을 의미한다.

 

유니티는 Camera, AssetBundle, Halo와 같은 핵심 모듈을 삭제하지 않는다.

WebGL 빈 프로젝트로부터의 모듈 삭제

모듈 제거는 엄청난 양의 메모리를 절약한다. 예를 들어 물리 모듈은 gzip 형식으로 압축된 ASM.js 코드가 5MB를 차지하는데 이를 제거하면 총 빌드 크기가 17MB에서 12MB로 감소한다.

C# Code Stripping

UnityLinker는 GC와 유사하게 *basic mark와 sweep principle로 작동한다. UnityLinker는 build에 있는 각 어셈블리에 포함된 타입과 메서드 맵을 작성한다. UnityLinker는 몇몇 타입과 메서드를 "roots"로 표시하고 타입과 메서드의 의존성 그래프를 **둘러본다.

 

예를 들어 한 타입의 메서드가 다른 타입의 메서드를 호출하면, UnityLinker가 호출된 타입과 메서드를 "in-use" 상태로 표시한다. UnityLinker가 roots 의존성을 모두 표시하면 시스템은 어셈블리를 재작성, used로 표시되지 않은 타입과 메서드를 생략한다.

 

*역주: Mark and Sweep 알고리즘을 의미한 것으로 보이며 여기서 자세히 알아볼 수 있다.

**역주: walk라는 단어가 쓰였다. 그래프 이론의 그 walk인 것 같지만 확실하지는 않은데 더 좋은 표현 있으면 제보를 바란다.

Scenes, Resources, Assmblies, 그리고 AssetBundles의 Roots

UnityLinker는 씬이나 리소스의 컨텐트에 포함된 내부 클래스를 roots로 표시한다. 비슷하게, 유저 어셈블리에 있는 모든 타입과 메서드도 roots로 표시한다.

 

만약 씬에 직접 있는 다른 어셈블리나 리소스에 포함된 에셋의 메서드나 타입을 사용하는 경우, 유니티는 이를 roots로 표시한다.

 

추가적인 타입이나 메서드를 roots로 표시하길 원하면 link.xml 파일을 사용하면 된다.

 

만약 프로젝트가 AssetBundles를 사용한다면 BuildPlayerOption.assetBundleManifestPath를 통해 추가적인 타입이나 메서드를 roots로 표시하는데 쓸 수 있다.

User Assemblies

유저 어셈블리는 Asset 폴더에 있는 loose code에서 유니티가 생성하는 어셈블리이다. 유니티는 대부분의 C# 코드를 Assembly-CSharp.dll에 넣는 반면, Assets/Standard Assets/이나 Assets/Plugins/의 코드를 Assembly-CSharp-firstpass.dll에 놓는다.

 

코드베이스의 쓰지 않는 타입이나 메서드가 너무 큰 비중을 차지하는 경우, stable code를 pre-built 어셈블리에 넣고 UnitiLinker가 이를 제거하도록 허용하여 바이너리 파일의 크기와 빌드 시간을 줄일 수도 있다.

 

Stable code를 pre-built 어셈블리로 넣는 것은 *Assembly Definition Files를 사용함으로써 할 수 있다.

 

*역주: Assembly Definition Files에 대해선 뒤에 설명된다.

Generic Sharing

참조 형식의 경우, IL2CPP는 참조 형식을 사용하여 제네릭끼리 공유가 가능한 구현을 생성한다.(C++ 코드) 허나, IL2CPP는 값 형식을 공유하지 않는데 그 이유는 IL2CPP는 각 타입마다 별도의 코드를 생성해서 코드 크기가 커지기 때문이다.

 

눈치챌만한 퍼포먼스 변화가 나타나지 않는 게 일반적이나, 특정 유스 케이스나 무엇을 위해 최적화하는지에 따라 달라진다. 보통은 클래스는 힙에 있고 스트럭트는 스택에 있다(코루틴 같은 예외를 빼고). 메모리 퍼포먼스와 사용에 대해선 중요하며, non-참조 형식을 사용하면 다른 문제가 발생한다. 성능에 영향을 미치려면 값 형식을 사용해 함수의 파라미터를 복제해야 한다. 이에 관해 자세한 내용은 이 블로그를 참조하도록 한다. 지금은 Integer와 Enum 타입이 공유되지 않는다는 것에 주의한다.

Assembly Definition Files

Assembly Definition Files은 managed assmblies를 정의하고 폴더마다 사용자의 스크립트를 할당할 수 있게 해 준다.

 

유니티는 스크립트 변경에 따라 영향을 받는 어셈블리만 빌드하게 되므로 iteration time이 빨라지는 결과를 볼 수 있다.

 

참고: 여러 어셈블리가 모듈화를 허용하는 만큼 애플리케이션의 바이너리 사이즈와 런타임 메모리 역시 증가한다. 테스트에 따르면 executable 파일은 어셈블리당 최대 4kB까지 증가할 수 있다.

Build Report

Build Report는 UI가 없고 유니티에 들어있는 API이다. 프로젝트를 빌드하면 빌드 리포트가 나오는데 이 빌드 리포트는 executable 파일에서 무엇이 제거(strip)했고 왜 제거했는지 알려준다.

 

제거 정보를 프리뷰 하려면...

1. 프로젝트를 빌드한다.

2. 에디터 창을 나가서

3. http://files.unity3d.com/build-report/ 여기에 접속한다.*

 

Build Report 툴은 현재 실행 중인 유니티 에디터에 연결하고 빌드 리포트의 명세를 다운로드 및 표시한다.

binary2text 툴을 사용하여 Library/LatestBuild.buildreport의 데이터를 볼 수 있다. 여기서 binary2text 툴은 맥의 Unity.app/Contents/Tools/ 혹은 윈도우의 Unity/Editor/Data/Tools/ 경로에서 제공된다. Build report는 유니티 5.5 이상부터 사용할 수 있다.

 

*역주: 접속하면 유니티 5.4 빌드 리포트가 뜨는데 deprecated 상태인지는 확인하지 않았다. 최신 버전은 다음 링크에서 확인할 수 있는 것으로 보인다.

 

https://docs.unity3d.com/kr/2019.4/Manual/com.unity.build-report-inspector.html

 

빌드 보고 인스펙터 - Unity 매뉴얼

 

docs.unity3d.com

4. Native Memory

네이티브 메모리는 대부분의 엔진 코드가 상주 메모리(resident memory)에 있기 때문에 애플리케이션 최적화의 핵심 컴포넌트이다.

 

코드를 Native Plugins로 통합하면 이를 직접 제어할 수 있지만 유니티 내부 시스템에서 네이티브 메모리 소비를 제어하고 최적화할 수 있는 것은 아니다.

 

내부 시스템은 다른 버퍼와 리소스를 사용하고 메모리 소비에 어떤 영향을 미치는지는 명백하지 않다.

 

다음 섹션에서 유니티 내부 시스템의 디테일을 살펴보고 네이티브 프로파일러에 자주 보이는 메모리 데이터에 대해 설명하도록 한다.

Native Buffer

유니티는 다양한 네이티브 할당자와 버퍼를 사용한다. 상수 버퍼와 같은 것들은 영속적이고, Back 버퍼와 같은 것들은 동적이다.

 

다음 서브섹션은 버퍼들과 동작에 대해 설명한다.

Scratchpad

유니티는 상수들을 4MB 버퍼 풀에 저장하고 프레임과 프레임 사이에서 사이클을 돈다. 이 풀은 라이프타임에 GPU에 바인딩되며 XCode나 스냅드래곤과 같은 프레임 캡처 툴에 의해 표시된다.

Block Allocator

유니티는 일부 내부 시스템에 블록 할당자를 사용한다. 메모리의 새로운 페이지 블록을 할당할 때마다 메모리와 CPU 오버헤드가 발생한다.

 

대체적으로 페이지의 블록 사이즈는 유니티가 시스템을 처음 사용할 때만 할당이 일어날 정도로 충분하다. 첫 할당 이후 페이지 블록은 재사용된다.

 

여기에 내부 시스템(에셋, 오디오 등)들이 블록 할당자를 사용할 때 작은 차이점들이 있다.

*AssetBundles

AssetBundle을 처음 로드할 때, 블록 할당자가 추가 CPU와 메모리 오버헤드가 필요하므로 **spin up을 한다. 그리고 AssetBundle 시스템이 메모리의 첫 페이지 블록을 할당할 수 있도록 허용한다.

 

유니티는 AssetBundle 시스템의 할당한 페이지들을 재사용하지만, 한 번에 많은 AssetBundle을 로드하려고 할 때 2~3번째 블록을 할당할 수도 있다.

 

애플리케이션이 종료되기 전까지는 할당이 유지된다.

 

*역주: spin up은 네이버 오픈사전에 instantiate와 동의어로 적혀있다.

**역주: 에셋 번들에 대해선 여기를 참조하라.

Resources

리소스는 다른 시스템과 공유되는 블록 할당자는 사용하는데, 그래서 처음에 리소스를 로드할 때는 CPU와 메모리 오버헤드가 없다.(애플리케이션 시작 시 이미 시행됨)

Ringbuffer

유니티는 텍스쳐를 GPU로 push 하기 위해 ring buffer를 사용한다. asyncUploadBufferSize를 통해 비동기 텍스처 버퍼를 조정할 수 있다.

 

참고: 유니티가 한 번 할당한 ring buffer는 다시 시스템으로 반환할 수 없다.

Assets

에셋은 런타임 동안 native, managed 메모리의 함축을 유발한다.

 

유니티는 managed 메모리를 넘어 네이티브 메모리가 더 이상 필요하지 않을 때 OS에 이를 반환한다.

 

바이트 카운트 - 특정 모바일 기기까지 네이티브 런타임 메모리를 절약하기 위해 다음과 같은 것들을 해볼 수 있다.

  • 메쉬에서 쓰지 않는 채널 제거
  • 애니메이션에서 중복 키프레임 제거
  • 빌드할 때 LODGroup의 상위 디테일의 메쉬를 제거하기 위해 Quality Settings에서 maxLOD 사용
  • Editor.log를 빌드 후에 확인하여 디스크에 있는 각 에셋의 크기가 런타임 메모리 사용에 비례하는지 확인하기
  • *GPU 메모리로 업로드되는 메모리의 양을 줄이기 위해 Quality Settings의 Rendering 섹션에 있는 Texture Quaility에서 밉맵을 통하는 텍스처 해상도를 낮추도록 강제하기
  • 노말 맵은 디퓨즈 맵과 1:1 크기를 유지할 필요가 없다. 따라서 시각적으로 선명함이 차이나지 않는 선에서 노말 맵의 해상도를 낮춰 디스크 공간을 절약한다.

Managed 메모리의 함축은 managed 힙의 육중한 파편화로 네이티브 메모리에서 발생하는 문제를 능가할 수 있음을 주의해야 한다.

 

*Texture Quality는 Project Settings에 있다.

Cloned Materials

아무 렌더러의 머터리얼에 접근하는 것은 심지어 아무것도 할당하지 않았더라도 머터리얼이 복사되므로 이 cloned material에 대해 주의해야 한다.

 

이 복제된 머터리얼은 GC에 수집되지 않고 오로지 씬이 바뀌거나 Resources.UnloadUnusedAssets()이 호출될 때만 제거된다. Read-only 머터리얼에 접근하길 원하면 customRenderer.sharedMaterial를 사용할 수 있다.

Unloading Scenes

UnloadScene()을 호출하여 씬과 연관되었지만 로드되지 않은 GameObject 객체들을 제거한다.

 

참고: 이것은 연관 에셋을 unload 하지는 않는다. 그런 걸 하거나 managed, native 메모리를 해제(free)하려면 씬이 unload 된 후 Resources.UnloadUnusedAssets()를 호출해야 한다.

Audio

Virtual Voices

유니티는 보이스들을 virtual 혹은 real로 설정하는 것을 동적으로 할 수 있는데, 플랫폼의 real time audibility에 의존한다.

 

예를 들어, 유니티는 멀리서 재생되고 작은 볼륨인 사운드를 virtual로 설정하지만, 가까이 오거나 커지면 real voice로 바꾼다.

 

Audio Settings의 기본 값들은 모바일 기기에 최적화된 값으로 설정되어 있다.

  Max Virtual Voice Count Max Real Voice Count
Default 512 32
Maximum 4095 255

DSP Buffer Size

유니티는 mixer latency를 제어하기 위해 DSP buffer size를 사용한다. 근본적인 오디오 시스템 FMOD는 플랫폼 의존적인 DSP buffer size를 정의한다.

 

버퍼 사이즈는 레이턴시에 경향을 미치므로 신중히 다뤄야 한다. 버퍼의 개수는 기본적으로 4개다.

 

유니티의 오디오 시스템은 Audio Settings에서 다음과 같은 샘플 카운트를 따른다.

Latency = Samples * Number of buffers Samples Number of Buffers
Default iOS & Desktop: 1024
Android: 512
4
Best latency 256 4
Good latency 512 4
Best performance 1024 4

Audio Import Settings

런타임 메모리와 CPU 퍼포먼스를 살리기 위해 올바른 설정을 해야 한다.

  • 스테레오 사운드가 필요 없으면 오디오 파일의 Force to mono 옵션을 활성화하라. 이러면 런타임 메모리와 디스크 공간을 절약할 수 있다. 이는 모노 스피커가 탑재된 대부분의 모바일 플랫폼에 사용된다.
  • 큰 AudioClip을 Streaming으로 설정하라. 유니티의 Streaming은 200KB 오버헤드를 발생시키므로 200KB보다 작은 오디오 파일은 Compressed into Memory로 설정한다.
  • 긴 클립에 대해 AudioClip을 Compressed into Memory로 설정하여 런타임 메모리를 절약한다.(만약 클립이 Streaming으로 설정이 되지 않았다면)
  • Decompress On Load는 메모리가 많지만 CPU 퍼포먼스로 인해 제한될 경우에만 사용해라. 이 옵션은 엄청난 양의 메모리를 필요로 한다.

런타임 메모리와 디스크 공간을 절약하기 위해 다양한 플랫폼은 선호하는 압축 포맷이 있다.

  1. ADPCM은 사운드 이펙트같이 아주 짧고 자주 재생되는 클립에 적용한다. ADPCM은 3.5:1의 고정 압축 비율을 가지고 있고 압축을 해제하는데 적은 비용이 든다.
  2. 안드로이드에서 긴 클립을 재생할 때 Vorbis(ogg)를 사용해라. 유니티는 하드웨어 가속 디코딩을 지원하지 않는다.
  3. iOS에서는 Vorbis 혹은 MP3를 써라.
  4. MP3와 Voribis는 압축을 푸는데 많은 리소스가 필요하지만 파일 사이즈는 효과적으로 작다. 고수준 MP3는 압축을 해제하는데 보다 적은 리소스를 요구하고 중간, 저품질 파일의 경우에도 압축 해제에 거의 비슷한 CPU 시간을 소비한다.
  5. 팁: 긴 루프 사운드에 대해서 Vorbis를 사용하면 루프를 처리하기 좋다. MP3의 데이터 블록은 미리 결정된 크기를 가지므로 루프가 정확히 데이터 블록 크기에 나누어 떨어지지 않으면 중간에 소리 공백이 생긴다. Vorbis는 그렇지 않다.

5. Android Memory Management

안드로이드의 메모리는 여러 프로세스가 공유한다. 프로세스가 메모리를 얼마나 사용하는지는 얼핏 보기에 명확하지 않다. 안드로이드의 메모리 관리는 복잡하지만 구글 I/O 2018에서 나온 훌륭한 대화가 있어서 이 글을 계속 읽기 전에 봐야 한다.

Paging on Android

페이징은 메인 메모리의 데이터를 부차적인 메모리(디스크)에 옮기거나 혹은 그 반대를 하는 기법이다.

 

안드로이드는 페이지를 디스크로 보내지만 페이징 과정에서 스왑 공간을 사용하지 않는다. 이는 특히 안드로이드의 모든 애플리케이션이 자체 Dalvik VM 인스턴스를 구동하는 다른 프로세스에서 실행되므로 전체 메모리를 보는 것을 더 어렵게 한다.

Paging vs swap space

안드로이드는 페이징 기법을 쓰지만 스왑 공간을 활용하지는 않는다. 페이징은 파일을 memory map(mmap()) 하고 필요에 따라 커널 페이지를 데이터에 저장하는 능력의 크게 의존한다.

 

이런 일은 자주 일어나지 않더라도 가용 메모리 용량이 낮고 시스템이 캐시 페이지 파일을 삭제하면 페이징은 커널 페이지들을 없어야 한다. 모바일 기기에서의 페이징은 배터리 수명을 줄게 하고 메모리의 과도한 마모(wear and tear)를 유발하므로 안드로이드는 더티 페이지를 페이징 하기 위한 공간을 스왑 하지 않는다.

Onboard flash

안드로이드 기기는 대부분 데이터를 저장하기 위해 작은 온보드 플래시와 제한된 공간을 가지고 있다. 이것은 주로 앱들을 저장하지만 스왑 파일을 저장하는데 쓰일 수 있다.

 

온보드 플래시는 느리고 일반적으로 access rate가 하드 디스크나 플래시 드라이브보다 느리다. 비록 최근에는 온보드 플래시의 크기가 증가했지만, 여전히 공간 스왑을 효과적으로 하기에는 충분하지 않다.

 

커널의 .config 파일 (CONFIG_SWAP)을 수정함으로써 스왑 서포트를 할 수 있지만 그것은 이 가이드에서 벗어난다.

Memory Consumption Limitations

안드로이드 시스템(메모리 킬러라 불리는)이 활성화되고 프로세스들을 끝내기 전 앱에서 얼마나 많은 메모리를 쓸 수 있을까? 불행하게도 명확한 답이 없다. 그리고 그것을 알아내는 건 dumbsys, procrank, 그리고 안드로이드 스튜디오와 같은 툴들을 통한 엄청난 프로파일링이 수반된다.

 

당신이 안드로이드의 메모리 소비량을 측정하는 능력에는 많은 요소가 영향을 미친다.

  • 서로 다른 플랫폼 구성. 예를 들면 저사양, 중사양, 고사양 기기들
  • 테스트할 기기의 서로 다른 OS 버전
  • 메모리 측정 시 앱의 서로 다른 지점(points)
  • 전체적인 기기의 메모리 pressure(메모리 양을 의미함)

항상 동일한 플랫폼 구성, OS 버전, 그리고 메모리 pressure를 가지고 코드의 동일한 위치에서 측정하는 것이 중요하다.

Low and high memory pressure

메모리 프로파일을 하는 좋은 방식은 애플리케이션의 메모리 소비를 프로파일할 때 기기가 충분한 양의 여유 메모리(low memory pressure)를 가질 수 있도록 하는 것이다. 만약 기기에 충분한 여유 메모리가 없다면(high memory pressure) 안정적인 결과를 얻기 어렵다.

 

(그러므로) high memory pressure를 찾기 위해 프로파일링을 해도 어려운 물리적 한계가 존재함을 염두해야 한다.

 

만약 시스템이 이미 메모리 캐시를 쓰레싱 했다면, 앱을 프로파일링 하는 동안 불안정한 결과를 보여줄 것이다.

Dumpsys**

Dumpsys는 안드로이드 기기 위에 실행돼서 시스템 서비스와 애플리케이션에 대한 상태 정보를 덤프 하는 툴이다. Dumpsys는 시스템 정보에 쉽게 접근할 수 있게 해 준다.

 

  • 단순한 문자열로 표현된 시스템 정보를 얻을 수 있다.
  • 덤프 된 CPU, 램, 배터리, 저장소를 정보로 애플리케이션이 기기에 미치는 전반적인 영향을 확인할 수 있다.

 

제공되는 dumpsys를 사용하려면 다음 커맨드를 입력한다.

 

~$ adb shell dumpsys | grep "dumpsys services"

 

각 프로세스에 매핑된 물리적 램을 모두 합한 다음, 모든 프로세스를 더하면 그 결과는 실제 전체 램 용량보다 커질 수 있다. Dumpsys를 사용하면 각 자바 프로세스에 관한 명확한 정보를 얻을 수 있다.

 

Stat dumpsys는 앱 메모리와 연관된 다양한 정보를 제공한다.

 

dumpsys meminfo를 사용해 안드로이드의 시스템 메모리를 덤프 할 수 있다.

 

**역주: 글의 순서를 조금 바꿨다.

dumpsys meminfo

adb는 안드로이드에서 실행 중인 애플리케이션의 메모리 정보를 얻기 위한 많은 도구를 제공한다. Overview를 얻기 위한 가장 대중적이면서도 빠른 방법은 다음 커맨드를 입력하는 것이다.

 

adb shell dumpsys meminfo

 

위 커맨드는 각 자바 프로세스, native heap, 다양한 프로세스뿐만 아니라 이진 데이터, 그리고 시스템 정보를 보고한다.

 

이름,  번들 ID 혹은 pid를 통해 싱글 프로세스를 추적하여 다음 커맨드와 같이 유니티의 androidtest 앱의 세부 정보를 알아낼 수 있다. 여기서 androidtest 앱은 오직 스카이 박스나 기타 콘텐츠가 없는 메인 씬만 있는 빈 유니티 프로젝트로 메모리 측정을 위한 기준선을 잡을 수 있다.

 

adb shell dumpsys meminfo com.unity.amemorytest

 

다음은 위 커맨드를 넥서스 6P에서 실행한 결과다.

 

* Applications Memory Usage (in Kilobytes):  
* Uptime: 6815563691 Realtime: 10882940478  
*   
* ** MEMINFO in pid 20676 [com.unity.androidtest] **  
*                    Pss  Private  Private  SwapPss     Heap     Heap     Heap  
*                  Total    Dirty    Clean    Dirty     Size    Alloc     Free  
*                 ------   ------   ------   ------   ------   ------   ------  
*   Native Heap    31467    31448        0        0    51072    47261     3810  
*   Dalvik Heap     1872     1760        0        0    12168     7301     4867  
*  Dalvik Other      470      460        0        0                             
*         Stack      492      492        0        2                             
*        Ashmem        8        0        0        0                             
*       Gfx dev     3846     2036        0        0                             
*     Other dev        4        0        4        0                             
*      .so mmap    17760      516    15908      161                             
*     .jar mmap        4        0        4        0                             
*     .apk mmap      243        0        0        0                             
*     .dex mmap      116        4      112        0                             
*     .oat mmap     6206        0     3244        0                             
*     .art mmap     2571      716      232       22                             
*    Other mmap       49        4        0        2                             
*    EGL mtrack    99840    99840        0        0                             
*     GL mtrack    64480    64480        0        0                             
*       Unknown     1270     1264        0       14                             
*         TOTAL   230899   203020    19504      201    63240    54562     8677  
*    
*  App Summary  
*                        Pss(KB)  
*                         ------  
*            Java Heap:     2708  
*          Native Heap:    31448  
*                 Code:    19788  
*                Stack:      492  
*             Graphics:   166356  
*        Private Other:     1732  
*               System:     8375  
*    
*                TOTAL:   230899       TOTAL SWAP PSS:      201  
*    
*  Objects  
*                Views:        7         ViewRootImpl:        1  
*          AppContexts:        2           Activities:        1  
*               Assets:        2        AssetManagers:        2  
*        Local Binders:       16        Proxy Binders:       21  
*        Parcel memory:        5         Parcel count:       23  
*     Death Recipients:        1      OpenSSL Sockets:        2  
*             WebViews:        0  
*    
*  SQL  
*          MEMORY_USED:        0  
*   PAGECACHE_OVERFLOW:        0          MALLOC_SIZE:        0  
*

 

다음 테이블에서는 결과를 비교하고 자세한 stat을 설명한다.(**번역은 생략한다.)

 

Area Empty Scene [MB] Full Scene [MB] Description
Pss 230 949 Proportional set size (Pss) is a metric the kernel computes that takes memory sharing into account. The system scales each page of RAM in a processby the ratio of the count of other processes using the same page. All private pages contribute 100% of their size, and shared memory contributes size/(num of processes shared). For example, a page that is shared between two processes will contribute half of its size to the Pss of each process. This way you can calculate the total RAM used by summing up the Pss across all processes. Comparing Pss between processes provides a rough idea of their relative weight.
Private Dirty 203 825 The most interesting and expensive metric is Private Dirty, which is the amount of RAM inside the process that cannot be paged to disk as it is not backed by the same data on disk, and the system cannot share with any other process. Another way to look at this is that this is the RAM that the system will reclaim when the application is destroyed. After reclaiming, it is quickly subsumed into caches and other uses because the system must fully utilize the limited memory available.
Native Heap 51 328 The Native Heap represents memory used by the process itself such as Unity Engine Code, Native C mallocs, and Mono VM.
Dalvik Heap 12 12 Dalvik Heap is memory the Dalvik VM allocates, for example; Variables in the Unity Java Android code.
Dalvik Other 0.4 0.4 Dalvik Other is memory used for JIT and Android GC.
Clean Memory 19 26 Android shares pages of memory among several processes such as code of common frameworks. As soon as memory in a page changes, the system must write to and modify the memory and flags the memory as dirty. However, clean memory is memory that hasn’t changed from when it was loaded from disk. If a change occurs, the memory becomes dirty.
Swapped Dirty 0.2 0.2 The application uses Dirty memory as space for computations. Android does not have a swap mechanism so dirty memory is also RAM that will be freed when the app exits. However, Swapped Dirty is used on some Android devices with the ability to remap, but they swap to RAM rather than flash. On Android, this is similar to Linux. ZRAM can compress pages and the Linux kernel swaps them to a special RAM area and decompresses them again when needed.
EGL mtrack 99 22 This is gralloc memory usage. It's primarily the sum of the SurfaceView and TextureView. It includes the frame buffer as well and therefore the size depends on the dimension of the framebuffers. The bigger the supported screen resolution, the higher the EGL mtrack number. In this test, the resolution of frame buffer for the full Scene was reduced to ensure good performance. Reducing the frame buffer size also reduces the amount of memory needed by these buffers.
GL mtrack & Gfx dev 69 581 GL and Gfx are driver-reported GPU memory, and are primarily the sum of GL texture sizes, GL command buffers, fixed global driver RAM overheads, and Shaders. Note that this number does not appear on older Android versions. Note: The client space driver and kernel space driver share a memory space. In some Android versions this sometimes gets counted twice and therefore the Gfx dev is bigger than it is in reality.
Unknown 1.3 6.5 Unknown is any RAM page that the system could not classify into one of the other more specific items. This includes native allocations or runtime metadata, which the tool cannot identify when collecting this data due to Address Space Layout Randomization. Private Dirty is unknown RAM dedicated to only your application.

Procrank

dumpsys를 하는 다른 수단은 바로 procrank이며 역시 모든 프로세스에서 메모리 사용량을 볼 수 있는 유용한 툴이다. 프로세스의 메모리 사용량을 내림차순으로 보여준다. 프로세스 별로 리포트되는 크기들은 Vss, Rss, Pss, 그리고 Uss이다.

 

adb shell procrank

 

* PID      Vss      Rss      Pss      Uss  cmdline 
*  890   84456K   48668K   25850K   21284K  system_server 
* 1231   50748K   39088K   17587K   13792K  com.android.launcher2 
*  947   34488K   28528K   10834K    9308K  com.android.wallpaper 
*  987   26964K   26956K    8751K    7308K  com.google.process.gapps 
*  954   24300K   24296K    6249K    4824K  com.unity.androidmemory 
*  888   25728K   25724K    5774K    3668K  zygote 
*  977   24100K   24096K    5667K    4340K  android.process.acore

 

  • Vss(Virtual set size) - Vss는 프로세스가 가지고 있는 접근 가능한 주소 공간의 전체 크기이다. Vss는 가상 메모리가 프로세스에 얼마나 관련되어 있는지 보여준다.
  • Rss(Resident set size) - Rss는 얼마나 많은 물리적 페이지가 프로세스에 할당되었는지 보여준다. 프로세스끼리 공유하는 페이지는 여러 번 중복으로 센다.
  • Pss(Proportional set size) - Pss는 Rss와 비슷하지만 공유된 페이지를 분산한다. 예를 들어 3개의 프로세스가 9MB를 공유하고 있으면 각 프로세스가 3MB의 Pss를 가져가게 된다.
  • Uss(Unique set size) - Uss는 Private Dirty로도 알려져 있다. 기본적으로 디스크의 동일한 데이터에 의해 백업되지 않고 다른 프로세스와 공유되지 않기 때문에 디스크에 페이징이 불가능한 프로세스 내부 RAM 크기이다.

참고: Pss와 Uss는 meminfo와 다른 리포트를 보여준다. Procrank는 데이터를 수집하는 커널 메커니즘이 meminfo와 다르기 때문에 결과 역시 달라진다.

Meminfo

Meminfo 커맨드는 전체 메모리 사용량의 요약을 보여준다.

 

adb shell cat /proc/meminfo

 

* MemTotal:        2866492 kB  
* MemFree:          244944 kB  
* Buffers:           36616 kB  
* Cached:           937700 kB  
* SwapCached:        13744 kB

 

이 중에서 몇 가지 유용한 정보들을 설명한다.

 

  • MemTotal은 커널과 userspace에서 사용 가능한 메모리의 총 양으로, handset이 GSM, 버퍼 등을 위해 메모리를 필요로 하므로 실제 물리적 RAM보다 작다.
  • MemFree는 사용되지 않은 RAM 용량을 의미한다. 안드로이드는 항상 프로세스 실행을 위해 사용 가능한 메모리를 사용하려고 하기 때문에 그 수는 대체로 많이 작다.
  • Cached는 파일 시스템 캐시 등에 사용된 RAM이다.

 

더 많은 정보가 필요하면 다음 두 링크를 참조한다.

 

https://developer.android.com/studio/profile/memory-profiler

 

메모리 프로파일러를 사용하여 앱의 메모리 사용량 검사  |  Android 개발자  |  Android Developers

끊김 현상, 멈춤, 심지어 비정상 종료를 일으킬 수 있는 메모리 누수 및 메모리 변동을 식별하는 데 도움이 되는 Android 프로파일러의 메모리 프로파일러 구성요소를 알아보세요.

developer.android.com

https://developer.android.com/topic/performance/memory.html

 

앱 메모리 관리  |  Android 개발자  |  Android Developers

Android용으로 개발할 때 사전에 메모리 사용량을 줄이는 방법을 알아봅니다.

developer.android.com

Android Studio

안드로이드 스튜디오는 안드로이드 SDK에서 활성화된 커맨드 라인 툴에 덧붙여 메모리 프로파일러를 제공한다. 커맨드 라인 툴에서 보고하는 것과 비슷하게 managed memory와 native memory를 분할한다.

 

 

이 경우 테이블은 dumpsys meminfo 섹션의 빈 프로젝트를 안드로이드 스튜디오의 데이터와 비교한다. 기본적으로 dumpsys meminfo에서 표시되는 App Summary와 몇몇 추가 사항을 다룬다.

 

Section Size [MB] Area
Total [mb] 168.7 All
Others [mb] 3.1 Other dev + Unknown
Code [mb] 28 mmaps
Stack [mb] 0.1 Stack
Graphics [mb] 88.7 Gfxdev + EGL mtrack + GL mtrack
Native [mb] 40.8 Native Heap
Java [mb] 8 Dalvik Heap

Plugins

보통은 대부분의 메모리가 Native Heap 섹션에 들어간다. Dalvik Heap은 Native Heap 섹션과 비교하여 작다. 만약 커지게 되면 안드로이드 플러그인을 조사해야 한다.

 

Native Heap은 메모리가 어디에서 왔는지 알기 어렵게 하고 프로파일러에서 Native Plugin을 볼 수 있는 좋은 방법이 없다.

 

더 좋은 통찰을 얻기 위한 솔루션은 써드 파티 통합에 사용된 플러그인을 떼어내고 측정한 다음 빈 프로젝트의 메모리 기준선과 비교하는 것이다.

6. Application Size

디스크 공간과 런타임 메모리를 줄이는 방법 중 하나는 애플리케이션(안드로이드, iOS를 의미)의 크기를 줄이는 것이다.

 

리소스와 코드는 런타임 메모리에서 직접적인 비중을 차지하므로 이걸 줄이면 런타임 메모리 역시 절약할 수 있다.

 

iOS에서의 IL2CPP 최적화에 관심이 있으면 이 아티클을 참고한다.

728x90
728x90