Unity WebGL의 메모리 이해

일부 사용자는 이미 메모리가 제한된 플랫폼에 익숙합니다. 데스크톱이나 웹플레이어를 사용하는 다른 사용자에게는 지금까지 문제가 되지 않았습니다.
콘솔 플랫폼은 사용 가능한 메모리 용량을 정확히 알고 있기 때문에 비교적 쉽게 타겟팅할 수 있습니다. 이를 통해 메모리에 예산을 책정하고 콘텐츠 실행을 보장할 수 있습니다. 모바일 플랫폼에서는 다양한 디바이스가 존재하기 때문에 상황이 조금 더 복잡하지만, 적어도 마켓플레이스 수준에서 가장 낮은 사양을 선택하고 저사양 디바이스를 블랙리스트에 추가할 수 있습니다.
웹에서는 불가능합니다. 모든 최종 사용자가 64비트 브라우저와 대용량 메모리를 사용하는 것이 가장 이상적이지만 현실은 그렇지 않습니다. 게다가 콘텐츠가 실행되는 하드웨어의 사양을 알 수 있는 방법이 없습니다. OS와 브라우저만 알고 있을 뿐 그 이상은 잘 모릅니다. 마지막으로 최종 사용자는 다른 웹 페이지뿐만 아니라 WebGL 콘텐츠도 실행하고 있을 수 있습니다. 그렇기 때문에 이것이 어려운 문제입니다.
다음은 브라우저에서 Unity WebGL 콘텐츠를 실행할 때 메모리에 대한 개요입니다:

이 이미지는 Unity 힙 외에도 브라우저의 메모리에 Unity WebGL 콘텐츠에 추가 할당이 필요하다는 것을 보여줍니다. 프로젝트를 최적화하여 사용자 이탈률을 최소화하려면 이를 이해하는 것이 매우 중요합니다.
이미지에서 볼 수 있듯이 여러 그룹의 할당이 있습니다: 웹 페이지가 로드되면 메모리에 영구적으로 유지되는 DOM, Unity 힙, 에셋 데이터 및 코드입니다. 에셋 번들, 웹오디오 및 메모리 FS와 같은 다른 기능은 콘텐츠에서 일어나는 일(예: 에셋 번들 다운로드, 오디오 재생 등)에 따라 달라집니다.
로딩 시, 32비트 브라우저의 일부 사용자에게 메모리 부족 문제를 일으키는 asm.js 구문 분석 및 컴파일 중에 브라우저의 임시 할당도 몇 가지 있습니다.
일반적으로 Unity 힙은 모든 Unity 전용 게임 오브젝트, 컴포넌트, 텍스처, 셰이더 등이 포함된 메모리입니다.
WebGL에서는 브라우저에서 공간을 할당할 수 있도록 Unity 힙의 크기를 미리 알아야 하며, 일단 할당된 버퍼는 축소되거나 커질 수 없습니다.
Unity 힙 할당을 담당하는 코드는 다음과 같습니다:
buffer = new ArrayBuffer(TOTAL_MEMORY);
이 코드는 생성된 build.js에서 찾을 수 있으며 브라우저의 JS VM에서 실행됩니다.
총_메모리는 플레이어 설정의 WebGL 메모리 크기에 의해 정의됩니다. 기본값은 256MB이지만 이는 저희가 임의로 선택한 값일 뿐입니다. 실제로 빈 프로젝트는 16MB만 있으면 작동합니다.
그러나 실제 콘텐츠에는 대부분의 경우 256 또는 386MB와 같이 더 많은 용량이 필요할 수 있습니다. 메모리가 많이 필요할수록 실행할 수 있는 최종 사용자의 수가 줄어든다는 점을 명심하세요.
코드가 실행되려면 먼저 코드가 있어야 합니다:
다운로드되었습니다.
를 텍스트 블롭으로 복사합니다.
컴파일됩니다.
이러한 각 단계에는 많은 양의 메모리가 필요하다는 점을 고려하세요:
- 다운로드 버퍼는 일시적이지만 소스 및 컴파일된 코드 버퍼는 메모리에 영구적으로 저장됩니다.
- 다운로드한 버퍼의 크기와 소스 코드의 크기는 모두 Unity에서 생성한 비압축 js의 크기입니다. 얼마나 많은 메모리가 필요한지 예측할 수 있습니다:
- 릴리스 빌드 만들기
- jsgz 및 datagz의 이름을 *.gz로 바꾸고 압축 도구로 압축을 풉니다.
- 압축되지 않은 크기는 브라우저의 메모리 크기이기도 합니다.
- 컴파일된 코드의 크기는 브라우저에 따라 다릅니다.
쉬운 최적화 방법은 엔진 코드 제거를 활성화하여 빌드에 필요하지 않은 네이티브 엔진 코드가 포함되지 않도록 하는 것입니다(예: 2D 피직스 모듈이 필요하지 않은 경우 제거됩니다). 참고: 참고: 관리 코드는 항상 제거됩니다.
예외 지원 및 타사 플러그인으로 인해 코드 크기가 커질 수 있다는 점에 유의하세요. 그렇긴 하지만, 널 검사와 배열 바운드 검사가 포함된 타이틀을 출시해야 하지만 전체 예외 지원으로 인한 메모리(및 성능) 오버헤드 발생을 원치 않는 사용자들을 보았습니다. 이를 위해 예를 들어 편집기 스크립트를 통해 --emit-null-check 및 --enable-array-bounds-check를 il2cpp에 전달하면 됩니다:
PlayerSettings.SetPropertyString("additionalIl2CppArgs", "--emit-null-checks --enable-array-bounds-check");
마지막으로 개발 빌드는 축소되지 않기 때문에 코드가 더 커지지만 최종 사용자에게 릴리스 빌드만 제공하므로 걱정할 필요는 없습니다... 맞죠? ;-)
다른 플랫폼에서는 애플리케이션이 영구 저장소(하드 드라이브, 플래시 메모리 등)에 있는 파일에 간단히 액세스할 수 있습니다. 웹에서는 실제 파일 시스템에 액세스할 수 없으므로 이 작업이 불가능합니다. 따라서 Unity WebGL 데이터(.data 파일)가 다운로드되면 메모리에 저장됩니다. 단점은 다른 플랫폼에 비해 추가 메모리가 필요하다는 것입니다(5.3부터 .data 파일은 lz4 압축 메모리에 저장됩니다). 예를 들어, 다음은 ~40MB 데이터 파일을 생성하는 프로젝트(256MB Unity 힙 사용)에 대해 프로파일러가 알려주는 내용입니다:

.data 파일에는 무엇이 있나요? 데이터.unity3d(모든 씬, 종속 에셋 및 Resources 폴더에 있는 모든 것), unity_default_resources 및 엔진에 필요한 몇 가지 작은 파일 등 유니티가 생성하는 파일 모음입니다.
에셋의 정확한 총 크기를 확인하려면 WebGL용으로 빌드한 후 Temp\StagingArea\Data에서 data.unity3d를 확인하세요(Unity 에디터를 닫으면 Temp 폴더가 삭제된다는 점을 기억하세요). 또는 UnityLoader.js에서 DataRequest에 전달된 오프셋을 확인할 수 있습니다:
new DataRequest(0, 39065934, 0, 0).open('GET', '/data.unity3d');
(이 코드는 Unity 버전에 따라 변경될 수 있습니다. 5.4 버전부터 적용됨)
실제 파일 시스템은 없지만 앞서 언급했듯이 Unity WebGL 콘텐츠는 여전히 파일을 읽고 쓸 수 있습니다. 다른 플랫폼과 가장 큰 차이점은 모든 파일 I/O 작업이 실제로 메모리에서 읽기/쓰기를 수행한다는 점입니다. 한 가지 알아두어야 할 점은 이 메모리 파일 시스템은 Unity 힙에 저장되지 않으므로 추가 메모리가 필요하다는 것입니다. 예를 들어 파일에 배열을 작성한다고 가정해 보겠습니다:
변수 버퍼 = 새 바이트 [10*1014*1024];
File.WriteAllBytes(Application.temporaryCachePath + "/buffer.bytes", buffer);
파일은 메모리에 기록되며 브라우저의 프로파일러에서도 볼 수 있습니다:

Unity 힙 크기는 256MB입니다.
마찬가지로 Unity의 캐싱 시스템은 파일 시스템에 의존하기 때문에 전체 캐시 저장소가 메모리에 백업됩니다. 무슨 뜻인가요? 즉, 플레이어 프리프와 캐시된 에셋 번들 같은 것들도 Unity 힙 외부의 메모리에서 영구적으로 유지됩니다.
웹글에서 메모리 소비를 줄이는 가장 중요한 모범 사례 중 하나는 에셋 번들을 사용하는 것입니다(익숙하지 않은 경우 매뉴얼 또는 이 튜토리얼을 확인하여 시작하세요). 그러나 사용 방식에 따라 메모리 사용량(Unity 힙 내부 및 외부)에 상당한 영향을 미쳐 32비트 브라우저에서 콘텐츠가 작동하지 않을 수 있습니다.
이제 에셋 번들을 실제로 사용해야 한다는 것을 알았으니 어떻게 해야 할까요? 모든 에셋을 하나의 에셋 번들로 덤프하시겠습니까?
아니요! 이렇게 하면 웹 페이지 로딩 시간에 대한 부담을 줄일 수 있지만, 여전히 (잠재적으로 매우 큰) 에셋 번들을 다운로드해야 하므로 메모리가 급증할 수 있습니다. AB가 다운로드되기 전 메모리를 살펴봅시다:

보시다시피 Unity 힙에는 256MB가 할당되어 있습니다. 그리고 이것은 캐싱 없이 에셋 번들을 다운로드한 후입니다:

이제 표시되는 것은 디스크의 번들과 거의 동일한 크기(~65MB)의 추가 버퍼로, XHR에서 할당된 것입니다. 이는 일시적인 버퍼일 뿐이지만 가비지 수집이 완료될 때까지 몇 프레임 동안 메모리 스파이크를 유발합니다.
그렇다면 메모리 스파이크를 최소화하려면 어떻게 해야 할까요? 각 에셋에 대해 하나의 에셋 번들을 생성할까요? 흥미로운 아이디어이긴 하지만 실용적이지는 않습니다.
결론은 일반적인 규칙은 없으며 프로젝트에 더 적합한 것을 선택해야 한다는 것입니다.
마지막으로, 에셋 번들을 완료하면 AssetBundle.Unload를 통해 언로드하는 것을 잊지 마세요.
에셋 번들 캐싱은 다른 플랫폼에서와 동일하게 작동하며, WWW.LoadFromCacheOrDownload를 사용하기만 하면 됩니다. 하지만 한 가지 중요한 차이점이 있는데, 바로 메모리 사용량입니다. Unity WebGL에서 AB 캐싱은 데이터를 영구적으로 저장하기 위해 인덱싱된 DB에 의존하는데, 문제는 DB의 항목이 메모리 파일 시스템에도 존재한다는 점입니다.
LoadFromCacheOrDownload를 사용하여 에셋 번들을 다운로드하기 전 메모리 캡처를 살펴보겠습니다:

보시다시피 512MB는 Unity 힙에, ~4MB는 기타 할당에 사용됩니다. 번들을 로드한 후입니다:

추가 필요 메모리가 최대 167MB로 증가했습니다. 이 에셋 번들에 필요한 추가 메모리(~64MB 압축 번들)입니다. 그리고 이것은 js vm 가비지 컬렉션 이후입니다:

메모리 파일 시스템에서 에셋 번들을 캐시하는 데 대부분 사용되므로 조금 나아졌지만 여전히 약 85MB가 필요합니다. 이는 번들을 언로드한 후에도 다시는 되돌릴 수 없는 메모리입니다. 또한 사용자가 브라우저에서 콘텐츠를 두 번째로 열면 번들을 로드하기 전이라도 해당 메모리 청크가 바로 할당된다는 점을 기억하세요.
참고로, 이것은 Chrome의 메모리 스냅샷입니다:

마찬가지로 에셋 번들 시스템에 필요한 또 다른 캐싱 관련 임시 할당이 Unity 힙 외부에 있습니다. 나쁜 소식은 최근 그 규모가 의도했던 것보다 훨씬 더 크다는 사실입니다. 하지만 좋은 소식은 곧 출시될 Unity 5.5 베타 4, 5.3.6 패치 6 및 5.4.1 패치 2에서 이 문제가 해결된다는 점입니다.
이전 버전의 Unity에서 Unity WebGL 콘텐츠가 이미 출시되었거나 출시가 임박한 상태에서 프로젝트를 업그레이드하지 않으려는 경우, 에디터 스크립트를 통해 다음 프로퍼티를 설정하는 빠른 해결 방법을 사용할 수 있습니다:
PlayerSettings.SetPropertyString("emscriptenArgs", " -s MEMFS_APPEND_TO_TYPED_ARRAYS=1", BuildTargetGroup.WebGL);
에셋 번들 캐싱 메모리 오버헤드를 최소화하는 장기적인 해결책은 LoadFromCacheOrDownload() 대신 WWW 생성자를 사용하거나 새로운 UnityWebRequest API를 사용하는 경우 해시/버전 파라미터 없이 UnityWebRequest .GetAssetBundle()을 사용하는 것입니다.
그런 다음 메모리 파일 시스템을 우회하여 다운로드한 파일을 인덱싱된DB에 직접 저장하는 XMLHttpRequest 레벨의 대체 캐싱 메커니즘을 사용합니다. 이것이 바로 저희가 최근에 개발한 것으로 에셋 스토어에서 사용할 수 있습니다. 프로젝트에 자유롭게 사용하고 필요한 경우 사용자 지정하세요.
5.3과 5.4에서는 LZMA와 LZ4 압축이 모두 지원됩니다. 그러나 LZMA(기본값)를 사용하면 LZ4/비압축에 비해 다운로드 크기가 작아지지만, WebGL에서는 실행이 눈에 띄게 지연되고 메모리가 더 많이 필요하다는 몇 가지 단점이 있습니다. 따라서 LZ4를 사용하거나 압축을 전혀 하지 않는 것이 좋으며(실제로 Unity 5.5부터는 WebGL에서 LZMA 에셋 번들 압축을 사용할 수 없습니다), LZMA에 비해 다운로드 크기가 커지는 것을 보완하기 위해 에셋 번들을 gzip/브로틀화하고 그에 맞게 서버를 구성할 수 있습니다.
에셋 번들 압축에 대한 자세한 내용은 매뉴얼을 참조하세요.
Unity WebGL의 오디오는 다르게 구현됩니다. 메모리에 어떤 의미가 있을까요?
유니티는 WebAudio를 통해 재생할 수 있도록 자바스크립트 영역에서 특정 오디오버퍼의오브젝트를 생성합니다.
웹오디오 버퍼는 Unity 힙 외부에 있으므로 Unity 프로파일러로 추적할 수 없으므로 브라우저 전용 툴로 메모리를 검사하여 오디오에 사용되는 메모리 양을 확인해야 합니다. 다음은 예제입니다(Firefox about:memory 페이지 사용):

이러한 오디오 버퍼에는 압축되지 않은 데이터가 저장되므로 대용량 오디오 클립 에셋(예: 배경 음악)에는 적합하지 않을 수 있다는 점을 고려하세요. 이러한 경우에는 <오디오> 태그를 대신 사용할 수 있도록 자체 js 플러그인을 작성하는 것이 좋습니다. 이렇게 하면 오디오 파일이 압축된 상태로 유지되므로 메모리 사용량이 줄어듭니다.
요약은 다음과 같습니다:
Unity 힙의 크기를 줄입니다:
'WebGL 메모리 크기'를 가능한 한 작게 유지하세요.
코드 크기를 줄이세요:
스트립 엔진 코드 활성화 예외 사용 안 함 타사 플러그인 사용을 피하세요.
데이터 크기를 줄이세요:
에셋 번들 사용 크런치 텍스처 압축 사용
예, 가장 좋은 전략은 메모리 프로파일러를 사용하여 콘텐츠에 실제로 필요한 메모리 양을 분석한 다음 그에 따라 WebGL 메모리 크기를 변경하는 것입니다.
빈 프로젝트를 예로 들어 보겠습니다. 메모리 프로파일러에서 "총 사용량"이 16MB를 조금 넘는다고 표시되므로(이 값은 Unity 릴리스마다 다를 수 있음), WebGL 메모리 크기를 이보다 더 큰 값으로 설정해야 합니다. 물론 '총 사용량'은 콘텐츠에 따라 달라질 수 있습니다.
그러나 어떤 이유로 프로파일러를 사용할 수 없는 경우 콘텐츠를 실행하는 데 필요한 최소 메모리 양을 찾을 때까지 WebGL 메모리 크기 값을 계속 줄일 수 있습니다.
또한 16의 배수가 아닌 값은 엠스크립트 요구 사항이므로 런타임에 자동으로 다음 배수로 반올림된다는 점에 유의해야 합니다.
WebGL 메모리 크기(MB) 설정에 따라 생성된 HTML의 총 메모리(바이트) 값이 결정됩니다:

따라서 프로젝트를 다시 빌드하지 않고 힙의 크기를 반복하려면 HTML을 수정하는 것이 좋습니다. 그런 다음 만족스러운 값을 찾으면 Unity 프로젝트에서 WebGL 메모리 크기를 변경할 수 있습니다.
다행히도 이 방법이 유일한 방법은 아니며, 다음 Unity 힙 블로그 게시물에서 이 질문에 대한 더 나은 답변을 제공하려고 합니다.
마지막으로 Unity의 프로파일러는 할당된 힙의 메모리를 일부 사용하므로 프로파일링할 때 WebGL 메모리 크기를 늘려야 할 수도 있습니다.
이는 Unity의 메모리 부족인지 브라우저의 문제인지에 따라 다릅니다. 오류 메시지에 문제의 내용과 해결 방법이 표시됩니다: "이 콘텐츠의 개발자인 경우 WebGL 플레이어 설정에서 WebGL 빌드에 메모리를 더 많이/더 적게 할당해 보세요." 그런 다음 WebGL 메모리 크기 설정을 적절히 조정할 수 있습니다. 하지만 OOM을 해결하기 위해 할 수 있는 일이 더 있습니다. 이 오류 메시지가 표시되는 경우

메시지 내용 외에도 코드 및/또는 데이터의 크기를 줄일 수도 있습니다. 그 이유는 브라우저가 웹 페이지를 로드할 때 코드, 데이터, 유니티 힙, 컴파일된 asm.js 등 여러 가지를 위한 여유 메모리를 찾으려고 하기 때문입니다. 특히 32비트 브라우저의 경우 데이터 및 Unity 힙 메모리가 상당히 커서 문제가 될 수 있습니다.
경우에 따라 사용 가능한 메모리가 충분하더라도 메모리가 조각화되어 브라우저에 오류가 발생하는 경우도 있습니다. 그렇기 때문에 브라우저를 다시 시작한 후 콘텐츠가 로드되는 경우가 간혹 있습니다.
다른 시나리오에서는 Unity의 메모리가 부족할 때 다음과 같은 메시지가 표시됩니다:

이 경우 Unity 프로젝트를 최적화해야 합니다.
콘텐츠에 사용되는 브라우저의 메모리를 분석하려면 Firefox 메모리 도구 또는 Chrome 힙 스냅샷을 사용할 수 있습니다. WebAudio 메모리는 표시되지 않으므로 Firefox에서 about:memory 페이지를 사용하여 스냅샷을 찍은 다음 "webaudio"를 검색할 수 있습니다. JavaScript를 통해 메모리를 프로파일링해야 하는 경우 window.performance.memory (Chrome 전용)를 사용해 보세요.
Unity 힙 내부의 메모리 사용량을 측정하려면 Unity 프로파일러를 사용하세요. 단, 프로파일러를 사용하려면 WebGL 메모리 크기를 늘려야 할 수도 있다는 점에 유의하세요.
또한, 빌드에 포함된 내용을 분석할 수 있는 새로운 도구도 개발 중입니다: 사용하려면 WebGL 빌드를 만든 다음 http://files.unity3d.com/build-report/ 을 방문하세요. 이 기능은 Unity 5.4부터 제공되지만, 이 기능은 현재 개발 중이며 언제든지 변경되거나 제거될 수 있습니다. 하지만 지금은 테스트 목적으로만 사용할 수 있도록 하고 있습니다.
16이 최소값입니다. 최대값은 2032년이지만 일반적으로 512 이하로 유지하는 것이 좋습니다.
이는 기술적인 제한 사항입니다: 2048MB(또는 그 이상)는 JavaScript에서 Unity 힙을 구현하는 데 사용되는 TypeArray의 32비트 부호 있는 정수 크기를 오버플로합니다.
힙의 크기를 조정할 수 있도록 ALLOW_MEMORY_GROWTH 엠스크립트 플래그를 사용하는 것을 고려했지만, 그렇게 하면 Chrome의 일부 최적화가 비활성화되기 때문에 지금까지는 사용하지 않기로 결정했습니다. 이러한 영향에 대한 실제 벤치마킹은 아직 수행하지 않았습니다. 이 기능을 사용하면 오히려 메모리 문제가 악화될 수 있습니다. Unity 힙이 너무 작아서 필요한 메모리를 모두 수용하지 못하고 더 커져야 하는 지점에 도달한 경우 브라우저는 더 큰 힙을 할당하고 이전 힙에서 모든 것을 복사한 다음 이전 힙의 할당을 해제해야 합니다. 이렇게 하면 새 힙과 이전 힙 모두에 동시에 메모리가 필요하므로(복사가 완료될 때까지) 총 메모리가 더 많이 필요합니다. 따라서 미리 정해진 고정 메모리 크기를 사용할 때보다 메모리 사용량이 더 많아질 수 있습니다.
32비트 브라우저는 OS가 64비트인지 32비트인지에 관계없이 동일한 메모리 제한에 직면하게 됩니다.
앞서 설명한 대로 Unity 프로파일러가 추적할 수 없는 Unity 힙 외부의 할당이 있기 때문에 브라우저 전용 툴을 사용하여 Unity WebGL 콘텐츠를 프로파일링하는 것을 마지막으로 권장합니다.
이 정보 중 일부가 도움이 되었기를 바랍니다. 추가 질문이 있는 경우 여기 또는 WebGL 포럼에서 주저하지 말고 질문해 주세요.
업데이트:
코드에 사용되는 메모리에 대해 이야기하면서 소스 JS 코드가 임시 텍스트 블롭에 복사된다고 언급했습니다. 저희가 발견한 것은 블롭이 제대로 할당 해제되지 않아 브라우저 메모리에 영구적으로 할당되었다는 것입니다. about:memory에서는 메모리-파일-데이터로 레이블이 지정됩니다:

크기는 코드 크기에 따라 다르며 복잡한 프로젝트의 경우 32메가바이트 또는 64메가바이트가 될 수 있습니다. 다행히 이 문제는 5.3.6 패치 8, 5.4.2 패치 1 및 5.5에서 수정되었습니다.
오디오의 경우 여전히 메모리 소모가 문제라는 것을 알고 있습니다: 오디오 스트리밍은 현재 지원되지 않으며 오디오 에셋은 현재 브라우저 메모리에 압축되지 않은 상태로 보관됩니다. 따라서 대용량 오디오 파일을 재생할 때는 <오디오> 태그를 사용하는 것이 좋습니다. 이를 위해 최근 오디오 소스를 스트리밍하여 메모리 소모를 최소화할 수 있는 새로운 에셋 스토어 패키지를 발표했습니다. 확인해 보세요!