본문 바로가기
유니티

유니티 메모리 관리, Garbage Collector, GC 최적화 접근방식, 유니티 원죄의 GC 스파이크의 답답함이란... 점진적 GC, Incremental GC는 해결책이 될까?

by NGVI 2021. 6. 21.

유니티 메모리 관리, Garbage Collector, GC 최적화 접근방식, 유니티 원죄의 GC 스파이크의 답답함이란... 점진적 GC, Incremental GC는 해결책이 될까?

유니티 프로젝트를 진행하다 보면 

초기에는 그냥 두다, 후기에 항상 큰 문제가 되는 메모리 부분 이야기를 조금 써보려 합니다.

 

최적화 힌트 제공보다는, 왜 유니티 GC가 느린가? 얼마나 느린가 등 정보가 많네요.

heap 공간에 할당되는 메모리

기본적으로는 힙 heap 공간에 할당되는 메모리에 대한 이야기입니다.

 

유니티 같은 경우는

Mono 프로젝트를 기반으로 돌아갑니다.

 

Mono는 또 뭔데?

오픈소스 개발자 그룹이 리눅스 환경 등 윈도우 이외의 환경에서도 NET Framework를 사용하기 위해 만든 프로젝트입니다. 유니티는 다양한 멀티플랫폼을 지원하고, 개발 역시 다양한 환경에서 가능합니다.

 

그래서, 다시 돌아와서

 

Mono란 녀석이 런타임 시스템에서 자동으로 메모리 관리를 수행한다는 것이 핵심입니다.

 

유니티의 메모리 관리 방식과 가비지 컬렉션 GC

프로그램에서 필요한 메모리가 있을경우 메모리 관리자는 미사용 힙 영역을 제공해야 합니다.

 

메모리 자원은 무한하지 않습니다. 한정적입니다. 

이전에 할당해둔 녀석들중 더 이상 유효하지 않은 녀석들이 있을 경우, 개발자가 직접 메모리에서 내릴 수 없습니다.

개발자는 단지 사용하지 않음 설정을 할 뿐입니다. 

 

메모리 관리자에서 특정 조건(잣은 요청, 메모리의 한계상황 등)과 상황에 따라 유효하지 않은 영역들을 정리하고 합니다.

 

러프하게 요렇게 돌아가는 구나 보시면 좋을듯합니다.

 

메모리를 파악하고 관리하는 프로세스를 가비지 컬렉션(Garbatge Collection) GC라고 보시면 됩니다.

 

유니티 GC는

Boehm–Demers–Weiser 가비지 컬렉터를 사용합니다.

위키 페이지는 아래

https://www.hboehm.info/gc/

 

기본적으로

Stop the world 방식의 GC입니다.

GC가 유효한 메모리를 수집할때, 프로그램 코드 실행의 중지, 가비지 컬 랙터의 작업 이후 다시 실행된다는 개념인데, 현재 유니티에서는 이렇게는 실행되고 있지 않지만, 그래도 상당한 시간을 소비합니다.

 

그리고 비세대, 비압축화입니다.

 

결국 문제가 되는 것은

GC가 메모리를 수집할 때, 기존 메모리 정리 작업도 같이합니다. 

코드로는 

system.gc.collect()가 될 것인데,

 

이때 느리다는 것입니다.

스파이크라는 단어가 사용될 수준입니다.

오웃, 스파이크

 

아니 왜?

유니티의 GC는 느린가?

기존. net의 GC를 조금 보신 분이라면,

메모리가 세대별로 할당돼서 관리되며, 힙 영역은  SOH(Small Object Heap)과 LOH(Large Object Heap)으로 구분돼서 관리되는 그런 거 아니야(이 정도는 하겠지)라고 상상하실 수 있는데,

 

유니티는 다릅니다.

 

이는 유니티의 원죄에 가까운 느낌입니다...

 

세대 구분, SOH, LOH, 메모리의 정렬

그런 거 없습니다.

스마트하지 못한 상당히 구시대적 GC

-2019 이전 버전까지는 확실하게 이랬습니다.

-2019 이후 버전 역시 개선이 되었고, 추가 개선 중이라곤 하는데, 만족할 수준은...

 

왜 유니티 GC가 저런 것인가 하면...

유니티 관련 개발자가 남긴 글을 좀 보면..

 

----

당신은 세부 사항에 주의를 기울였으므로 여기에 당신이 받아야 할 세부 사항에 대한 이야기가 나옵니다.

 

Unity와 Mono가 공동 작업을 발표한 것은 2008 년 초였으며 그 당시 Unity는 Mono 런타임 (오픈 소스 사용을 위한 GPL 포함)을 포함하도록 라이선스를 부여했습니다. 그리고 Boehm GC는 Mono의 기본 GC였습니다.

 

경과 시간 및 Mono 4.x / 5.x는 기본적으로 세대 / 압축 기능과 함께 SGen GC를 사용합니다. 그러나 Unity는 라이선스 비용을 다시 지불하고 싶지 않았습니다. 

 

따라서 문서가 그대로 남아 있음을 알 수 있습니다.

 

Microsoft는 2016 년에 Xamarin을 인수하여 Mono 핵심 자산을 제어했습니다. 

 

MIT에서 코드 기반을 다시 게시하여 라이선스 문제를 영원히 해결했습니다. Unity는 .NET Foundation에 합류하고 Microsoft / Xamarin과 협력하여 최신 Mono 런타임을 게임 엔진에 통합하기 시작했습니다.

 

이러한 노력은 아직 진행 중이며 곧 완성될 예정입니다 (현재 실험 기능).

 

BTW, Unity는 아직 표준. NET GC를 사용할 수 없습니다. Microsoft는. NET Framework에서 GC를 오픈 소스 하지 않지만. NET Core 버전입니다. 이 GC는 Mono의 것과 다르며 Unity에 포함하려면 더 많은 노력이 필요합니다. 

 

그것이 바로 지금 Mono 5가 통합된 이유라고 생각합니다. 앞으로 Unity는. NET Core GC로 마이그레이션 할 것입니다.

 

출처

https://stackoverflow.com/questions/46574407/unitys-garbage-collector-why-non-generational-and-non-compactin g

 

과거 구버전 mono사용 시 거기에 사용된 GC를 사용하고 있고

이게 개선은 하고 있는데 쉽지 않아..

뭐 이 정도 내용이네요..

 

그럼 얼마나 느린가?

개발사에서 공개한 매뉴얼에 따르면

 

- iOS에서 사용할 때 할당할 일반적인 힙 크기는 200KB이며, 가비지 컬렉션은 이 경우 iPhone 3G에서 대략 5ms 정도 걸리게 됩니다. 힙 크기가 1MB로 증가하면 가비지 컬렉션은 7ms 정도 걸리게 됩니다. 

 

라고 합니다.

 

정리할 메모리 용량이 아닌, 힙에 할당되어 있는 메모리가 기준입니다.

 

힙에 100m가 할당되어 있다고 할 때, GC가 작동되면 70ms입니다.

500m 할당되었다면 350ms를 소요하게 됩니다. 0.3~0.4초 정도...

분명 게임 유저가 있다면 말이 나올만한 수치입니다.

 

이는 현실적으로 굉장한 문제입니다.

 

어느 시점에서 프레임이 분명 튀어 보이게 됩니다.

참 답답한 이야기입니다.

 

그리고 GC 자체가 정확히 언제 불린다를 특정할 수 없는 것 역시 문제입니다.

 

기본적으로 메모리 사용량이 한계에 따르면 당연히 정리를 해야 하니 불립니다.

그런데 꼭 이럴 경우만이 아니고,

 

구동하고 있는 환경, 여러 어플에 메모리를 같이 당긴다고나,

플랫폼별 환경

GC할당 빈도

...

 

다양한 요소가 GC Collect를 호출한다는 것입니다.

 

어느 순간 갑자기 당신의 게임임 뚝뚝 끊어져 보일 수 있다는 겁니다. 답답..

 

유니티 GC 최적화를 위해서는 어떤 식을 접근을 해야 할까?

일단 먼저 프로파일링을 해봐야겠죠.

 

GC Alloc라는 부분은 여러분이 GC에 먹이를 주는 행위를 표시해 줍니다.

-사실 이건 케이스 바이 케이스로 분석해봐야 되는 내용이긴 합니다...

 

GC.Collect 가 실제 얼마나 부하가 있는지 확인해볼 수 있습니다.

 

분석해봤을 경우 문제가 있다 싶으면,

몇 가지 추천드릴 만한 내용이 있습니다.

 

2019 이상 버전을 사용할 때

 

점진적 GC를 사용하세요.

2019.1 알파 이후로 추가된 기능인 Incremental GC를 사용하세요

세팅 -> Player -> Use Incremental GC 체크

*Scripting Runtime Version 이 4.x부터 사용이 가능합니다.

 

코드를 통한 활성화 접근도 가능

GarbageCollector.GCMode = GarbageCollector.Mode.Enabled;
GarbageCollector.GCMode = GarbageCollector.Mode.Disabled;

 

부하가 있는 GC를 점진적으로 진행시킵니다.

하나의 작업을 여러 프레임 동안 나눠서 하는 계념이라고 보면 됩니다.

 

원칙적으로 GC에 사용되는 시간 총량이 줄어드는 효과가 아닙니다.

-음... 뭔가 아쉬운 개선

 

워크로드 분산으로 GC 스파이크 문제점을 개선시킬 수 있습니다.

 

점진적 GC 미사용
점진적 GC 미사용

초기 적용대상은

윈도, 맥, 리눅스, 안드로이드, ios

이후 +ps4, xbox one, nintendo switch, 유니티 에디터

 

점진적 GC 역시 여전히 Boehm–Demers–Weiser GC를 사용하고 있습니다.

원칙적으로 뭔가 좀 개선이 되야 할 거 같은데...

 

음.. 아직도 먼가 못 마땅한 느낌...

 

아래는

2019가 아니더라도 가능한 내용

작은 힙과 빈번한(주기적인) 가비지 컬렉션

주기적으로 GC를 행하라는 추천인데, 이게 먼 개소리여 라고 하실 수 있지만,

무려 유니티 자체 매뉴얼에서 권장하고 있는 내용입니다.

 

자체 매뉴얼은 아래 링크 첨부해둘 것이고요

해당 내용

일반적이지 않은 GC를 사용하는 유니 티기에 가능한 추천인듯합니다.

 

정말 heap가 적다면 시도해볼 수 있고,

메모리 관련 문제로 치명적인 오류가 있다면 적용해볼 만 한데,

 

좀 그렇죠..

 

강제적인 가비지 컬렉션

가장 무난한 전략으로, 좋은 타이밍에 GC를 미리 호출하는 것을 생각해볼 수 있습니다.

특정 위치에서 가비시 컬렉션을 강제로 효 출하는 걸 생각해볼 수 있습니다.

씬 전환, UI 오픈, 특정 이벤트 시작 등 뭔가 로딩이 있을 경우 하나씩 끼워주는 것입니다.

 

오브젝트 풀의 사용

게임에서 자주 사용하는 객체는 해제하기보단 재사용하는 편이 GC에도 당연히 도움이 되겠죠?

컬렉션과 배 열등도 재사용하는 게 좋습니다.

 

Resources.UnloadUnsuedAssets()

를 통해 사용하지 않는 리소스를 해제시키도록 하자.

 

참조 페이지

유니티 자체 매뉴얼 중, 자동 메모리 관리 이해 페이지

https://docs.unity3d.com/kr/2021.1/Manual/UnderstandingAutomaticMemoryManagement.html


유니티로 AAA급 제작의 어려움이 있다고 말할 때

늘 이야기되는 GC의 문제

 

좀... 큰 개선이 있어야 하지 않을까 

생각합니다.

댓글