Asset Bundles vs. Resources : A Memory Showdown

반응형

필요에 의해 번역한 내용입니다. 원문을 보시기를 추천합니다.


리소스 : Resources

에셋번들 : Asset Bundles


최근에 우리는 기존 방식의 리소스 시스템 사용에 대한 것과 

왜 이전 에셋 로딩이 이후 에셋 로딩보다 메모리 오버헤드가 더 높은지에 대해 많은 문의를 받았다.


본문이 너무 길어 읽지 않은 사람들을 위해 : 

만약 리소스 시스템에서 할 수 없는걸 에셋번들이 할 수 있다면 

결국에는 에셋번들의 오버헤드가 훨씬 작을 것이다.

만약 에셋번들에 익숙하지 않고 에셋번들에 대해 더 알고자 한다면 Unity Manual

Asset Bundles & Resources Guide를 읽어보길 추천한다.


바로 살펴보면, 우리는 기본적으로는 같은 내용인 버그 리포트를 몇개 받았는데,

내용은 에셋번들을 통해 에셋을 로드할 때는 메모리가 몇 메가바이트 증가하지만

리소스를 사용하면 메모리가 증가하지 않는다는 내용이다.

버그를 단계별로 재현해 보면 위의 내용과 매우 비슷한 결과를 얻을 수 있다.

일반적인 메모리는 시작때 사용되고 에셋이 로드될 때 증가하며, 원래 값으로 돌아가지 않는것을 발견했다.


에셋번들의 메모리 사용량


리소스의 메모리 사용량


위의 실제 수치들을 살펴보기 전에 이러한 수치를 나타내는 시스템과 그들 사이의 관계에 대해 이야기 해 보자.

유니티의 네이티브 메모리 시스템은 메인스레드 대 백그라운드 스레드 같이 어디에 할당되어 동작하는 타입인지, 

또는 현재 실행중인 플랫폼이 무었인지에 따라 1MB ~ 32MB(평균 1MB 에서 4MB)의 

다양한 크기의 몇몇개의 고정 크기의 블럭 할당자(메모리풀)를 사용한다 

Reserved Total(총 예약)은 OS에 의해 할당된 모든 블럭들의 합인 반면,

Used Total(총 사용)은 Reserved Total에서 유니티가 실제로 사용하는 것을 나타낸다.

FMOD, Profiler 등의 각 레이블은 해당 할당자의 설정 값이거나 시스템에서 보고된

예상 외부 메모리를 나타낸다.

레벨 영역에 대해서는 Memory Profiler 메뉴얼을 참고하면 된다.

메뉴얼 페이지에 없는 것 중 알아야 할 게 좀 있는데, Used Total과 Reserved Total은 

이 글을 쓸 당시에(Unity 5.5.0f3) FMOD 값을 포함하지 않고 있는데

이 부분은 수정하고 있는 중이다.

(앱이 시작될 때 리소스 기준으로 계산 해 보자면, Used Total : 30.1메가 = 

Unity : 21.4메가, Mono : 356킬로바이트, GfxDriver : 4.6메가, Profiler : 3.7메가)

Total System Memory Usage(총 시스템 메모리 사용 양)는 플랫폼에 의해 시스템에서 보고받은 

가상메모리 크기로 이는 모든 플랫폼의 특징은 아니며, 지원하지 않는 경우에는 0으로 표시된다.

마지막으로 Used Total은 오브젝트의 헤더나 바이트 정렬을 고려하지 않지만, Reserved Total은 고려한다.

그래서 Asset Bundles vs Resources의 메모리 사용량을 살펴보기 위해, 

유니티의 Used Total과 Reserved Total 부분을 둘 다 집중해서 살펴볼 것이다.


프로파일러에서 진행되는 작업의 이해를 위해 필요한 또 다른 정보 하나는

에셋번들과 리소스의 데이터가 디스크에 배치되는 방법이다.

리소스와 에셋번들 두 방식의 핵심은 데이터 구조가 무척 비슷하다는 것이며,

직렬화된 오브젝트를 로드하기 위한 에셋 파일 경로의 매핑과 

추가 리소스 파일의 효율적인 비동기 로딩(텍스쳐, 오디오 등)을 위해

리소스와 에셋번들 둘 다 모든 오브젝트에 대한 직렬화된 데이터를 포함하는 하나의 파일을 가진다.

에셋번들은 이러한 파일들을 아카이브에 같이 저장하고 매핑은 에셋번들 오브젝트 안에 직렬화된 데이터에 저장된다.

리소스는 매핑을 ResourceManager라는 전역 싱글톤에 저장하고 파일 자체는 디스크에서 없어진다.

게다가, 리소스 시스템과는 달리, 에셋들은 모두 같은 에셋번들에 있어야 할 필요가 없으므로

데이터를 일부만 로드하여 메모리 사용량을 보다 많이 제어할 수 있다.

좀 더 자세한 정보는 에셋번들 내부 구조에 관한 메뉴얼에서 확인할 수 있다.

(이 글을 쓰고 있는 지금도 에셋번들 내부 구조에 관한 메뉴얼 글이 계속 갱신되고 있는 중인 듯 하다.)


Asset Bundles

Resource

유니티의 메모리와 파일 시스템에 대한 지식을 활용하여, 

이제 메모리 사용량 수치가 무었을 나타내는지에 대해 좀 더 깊이있게 이야기 해 보자.

첫번째로 두드러지는 내용은 에셋번들의 Reserved Unity 영역이 10메가 증가한 반면,

(60.1메가 -> 70.1메가) Resource는 전혀 증가하지 않았다(63.3메가 -> 63.3메가)는 것이다.

왜 그럴까?

이는 실제로 앞서 언급했던 블럭 할당자 때문이다.

이 특별한 메모리 사용량 테스트를 위해 비동기 로딩 API와 코루틴을 사용하는 AsyncBundleLoader.cs 를 사용하였다.

이 조합은 현재 시점까지 아직 사용되고있지 않는 다른 블럭 할당자를 실제로 사용하기에 무척 주의해야 한다.

그래서 이 10메가의 증가는 초기 메모리 블럭 할당과, 새 오브젝트에 필요로 하는 추가 메모리로

4메가 블럭을 할당, 이렇게 두 할당이다.

두개의 새로운 할당 중, 하나는 에셋번들 비동기 로딩을 위해 2메가 블럭을 할당하고,

다른 하나는 Type Trees를 위해 4메가를 할당한다.(Type Trees에 대해 자세한건 나중에 이야기 하자)

이러한 블럭 크기들은 여러개의 Asset들을 동시에 로딩하는 것에 최적화 되어있다.

예를들어, 에셋번들 비동기 로딩을 위한 할당이나 새 블럭을 필요로 하는 Type Trees가 없어도

동시에 4~5개의 에셋번들에서 오브젝트들을 로드할 수 있다.

이는 당연히 이 번들에 에셋번들 크기와 압축 사용여부, 그리고 얼마나 많은 스크립트를 

사용했는지에 따라 다르다.


이러한 블럭 할당의 경우, Type Tree를 위한 4메가 블럭은 에셋번들에서 오브젝를 실제로 로딩할 때만 필요하다.

이 블럭은 사용후에는 사라져야 하지만, 예제에서 코루틴을 어떻게 구조화 했는지에 따라 

AssetBundleRequest 오브젝트는 여전히 사용중으로 간주되어 가비지 컬렉터에 의해 정리되지 않는다.

에셋번들 비동기 로딩을 위한 2메가는 에셋번들 아카이브 내의 버퍼를 읽는 스레드용으로

사용되며 번들에 대한 내부 참조가 없을 때 사라진다.

마지막 4 메가 블럭은 모든 오브젝트 저장에 사용되는 메인 할당자에 있기 때문에 사라지지 않는다.

일반적인 프로젝트에서는 오브젝트의 생성 및 삭제가 빈번하게 발생하므로 오브젝트를

해제하여 할당자에게 되돌려주는 대신 재사용을 위해 메모리를 풀링한다.

마지막으로 언로드된 Reserved Unity 값을 보면, 에셋번들의 값이(64.1 MB)

단지 할당자가 새 블럭을 얻는 순서때문에 Resource의 값(63.1 MB)에 매우 가깝다는 것을 알수 있다.


여태껏 Reserved 메모리에 대해 이야기 했는데, 에셋번들과 리소스 사이에

실제 Reserved 메모리를 사용하는 효율성은어떨까?

이는 유니티의 Used 영역이 바로 표시해 주기 때문에 인지하기가 무척 쉽다.

에셋번들의 경우 Reserved 메모리에서 21.7 MB를 사용하고, 

리소스는 에셋번들보다 조금 더 많은 22.2 MB를 사용한다.

게다가, 언로드 시킬 때, 이 메모리는 각각 20.7 MB와 21.2 MB로 떨어진다.

그래서 Asset Bundle이 효율적인 메모리 활용면에서는 분명하게 승리자이다.

이미 알고 있듯이, 언로딩 후 에셋번들 사용량은 시작 했을 때 보다 훨씬 커졌다.

(16.3 MB -> 20.7 MB , 4.4 MB가 커져있다.)

이전에 언급했었듯이 메모리 재사용을 위해 풀링되어 있기 때문에 에셋번들과 에셋을 

다시 로드하면 21.7 MB로  돌아간다.

리소스의 경우에는, 시작과 언로드 간의 메모리 차이는 반올림 오류 때문이다.


에셋번들을 위한 블럭 할당은 성능과 하위 호환성과의 절충으로 줄일 수 있다.

위에서 언급했듯이, 오브젝트를 로드하기에 충분한 메모리가 없기 때문에

4 메가 블럭 할당은 불가피 하다.

나머지 6 MB중 2 MB는 비동기로딩 API를 사용하기 때문에 발생한다.

그래서 블럭 할당을 피하기 위해서는, FPS 버벅거림을 감수하고 synchronous API을 사용하면 된다.

마지막 4 MB 할당은 Tree system 타입 때문이며, 위에서 언급했듯이 더 자세히 설명할 것이다.

이 시스템은 여분의 데이터를 Asset BUndle에 저장하지만, 리소스에는 저장하지 않으며,

이는 Asset Bundle을 보다 넓은 범위의 유니티 버전과 호환 가능하게 만들고

FromerlySerializedAsAttribute 와 같은 직렬화 속성을 작동시킨다.

이는 만약 새 버전으로 유니티를 업그레이드하거나 중요하지 않은 코드를 바꾸거나 할 때, 

그것들을 재빌드 하는 대신에, 게임 변경시 유저가 전체 에셋번들을 다시 다운로드 하게 할 때

동일한 에셋번들을 계속 사용할 수 있다.

이 추가 데이터를 쓰지 못하게 하려면 BuildAssetBundleOptions.DisableWriteTypeTree 옵션을

BuildPipeline.BuildAssetBundles API에 전달하면 된다.


Asset Bundles Loaded Synchronous without Type Trees


Asset Bundle 1, Resource 0 개 있다.

만약 이 데이터를 직접 재현하고자 한다면 이 블로그에서 사용된 스크립트가 Github Gist에 업로드 되어 있다.

현재 Texture, Monobehaviro, 그리고 Prefab를 100개씩 생성하도록 설정되어 있으며,

고정 랜덤화 시드를 사용하기때문에 실행 할 때 마다 동일한 결과가 나올 것이다.

(하지만 아마도 나의 결과와는 다를 수 있다.)

Asset Bundle 프로젝트에 실수로 컨텐츠가 있는 Resource폴더를 만들지 않도록 만들어야 하는데

그렇지 않다면 메모리 값이 예상보다 두배가 된다.



Reference Link

- 원문

- unity doc, The Profiler WIndow

- unity doc, Practical guide to optimization for mobiles

- unity doc, FromerlySerializedAsAttribute

- 파일 직렬화

- 파일 아카이브

- unity doc, 자동 메모리 관리를 이해하기

-

TAGS.

Comments