Native Memory
기본
Native Memory란 무엇인가? 유니티 엔진 내부의 C/C++ 코어가 쓰는 메모리 영역이다. 엔진 내부에서 쓰는 데이터들이 올라간다. 당연하게도, GC에서 자유롭다.
보통은 텍스처, 메쉬, 물리엔진, 오디오, 에셋로드 등이 Native Memory 영역을 이용한다.
메모리 세팅

Project Settings -> Memory Settings를 가면 다양한 메모리 관련 할당 정책의 값들을 정할 수 있다.
XXXBlockSize라는 값들이 많은데 이는 유니티가 힙 메모리에 자리가 부족할 때, OS에 메모리 추가 할당을 요청하는 단위이다. 만약 메모리가 제한적이라 최대한 알맞게 써야한다면 이 값을 줄일 수 있고, 대규모 메모리 사용이 가능하다면 이 값을 키워서 불필요한 요청을 줄일 수도 있을 것이다.
Type Tree Block Size같은 건 에셋의 메타데이터를 저장하는 단위이므로 이 값을 넉넉히 늘려둔다면 메모리 파편화를 막을 수 있을 것이다. (반대로 너무 늘리면 낭비겠지만)
추가적으로 Shared Bucket Allocator 관련 설정, Fast Per Thread Temporary Allocators(TLS), Fast Thread Shared Temporary Allocators등이 있는데, 일단 넘어가자.
Native Memory 사용하기
GC에서 자유로우니 성능도 좋을 것이고, GPU에 Native Memory에 있는 값을 바로 넘겨준다면 C#의 관리 메모리 => Native Memory로의 복사가 없어서 성능이 또 좋아질 것이고, 여러 정책까지 조절 가능하다니 성능이 더더욱 좋아질 것 같은 이 메모리. 유저가 사용할 수는 없을까?
답은 있다. Unity.Collections가 제공하는 NativeXXX 시리즈를 필두로 하는 각종 컬렉션들이다. Array, List, Hashset, HashMap 등등이 있는데 대표적으로 Native List만 알아보자.
Native List
Native Memory를 사용하게 해주는데, C#에서 List<T>쓰듯 딸깍 할 수는 없다. 사용자가 직접 메모리 할당/해제를 해줘야 한다. Native Collection에서 이는 Dispose를 호출함으로써 가능하다.
그리고, Blittable Type만 사용 가능하다.
var nativeData = new NativeList<int>(Allocator.Temp);
nativeData.Add(1);
nativeData.Dispose(); //안 해주면 오류난다.
NativeList도 초기 Capacity를 줄 수도 있고, Cap이 초과하면 자동으로 리사이징을 한다.
근데 이상한 놈이 보인다. Allocator.Temp는 무엇인가? Allocator를 이해해야 Native 컬렉션들을 잘 쓸 수 있다.
Allocator
딱 자주 쓸 3개만 일단 알아보자.
- Allocator.Temp : 딱 1프레임 정도만 살아있는 메모리이다. 1프레임을 넘기면 경고나 에러가 난다. 쓰레드 간 공유가 불가능하다(즉 job에서 못 쓴다). 엄청 빠르다.
- Allocator.TempJob : 4프레임 이내만 살아있는 메모리다. 마찬가지로 4프레임 넘기면 경고나 에러가 난다. Job에서 사용할 수 있다. 얘도 꽤 빠르다.
- Allocator.Persistent : 영구히 할당할 메모리이다. 물론 수동으로 해제해 줄 수 있다.
아, 쓰면 그냥 쓰는 거지 뭘 또 이렇게 나눠놨을까? 다 이유가 있는데, 그야 당연히 성능 때문이다.
Thread Local Storage Stack Allocator (TLS)
이름에 Stack이 붙어있다. 그렇다 Stack같은 놈이다. 힙에다가 메모리를 할당해놓고 스택처럼 쓰는거다. 내부에는 스택 포인터 같은 게 있다. 매 프레임 스택 포인터를 0으로 만들어서 다 초기화시킨다. 즉, 할당도 엄청빠르고 해제도 엄청 빠르다. Allocator.Temp가 이것을 사용한다. 근데 이놈 Stack처럼 LIFO이다. 만약 a,b,c 순서대로 할당해놓고 a,b,c순으로 해제하면 어떻게 될까?
public void Bar()
{
var a = new NativeList<int>(Allocator.Temp);
var b = new NativeList<int>(Allocator.Temp);
var c = new NativeList<int>(Allocator.Temp);
a.Dispose();
b.Dispose();
c.Dispose();
}
뭐, 어쩌피 1프레임 뒤에 다 해제되긴 한다. 그래도 큰 문제가 있다. a,b는 일단 1프레임 동안은 메모리에서 사라지지 않는다. 만약 Bar를 1000번 호출한다면 어떻게 될까? a,b가 계속 할당되면서 TLS가 모잘라지고 계속 새로운 힙 할당을 요구할 것이고, 느려지는데다가 과도하게 메모리를 차지할 것이다. LIFO에 맞게 해제해주자. 만약 이게 꽉 차면 할당은 다음에 볼 Thread-Linear 할당자로 넘어간다.
메모리 세팅에서 Fast per thread temporary allocator가 이 녀석의 할당 단위를 조절하는 영역이다.
Thread-safe linear allocator

이거는 Allocator.TempJob이 주로 쓰는 녀석이다. 보면 Block Size가 생각보다 작다. 얘는 Block들을 할당해서 풀에 넣어두고 하나씩 꺼내 쓴다. Block은 ring buffer로 되어있는데, TempJob으로 할당된 녀석들이 여기 차곡차곡 쌓이는 방식이다. Ring buffer를 쓰니까 Thread끼리 공유하지만 거의(?) lock-free이다. 그럼 얘도 TLS처럼 순서에 맞게 해제 안 하면 어떻게 되나? 싶을거다. 결론은 순서에 맞게 해제 안 해도 상관 없다. (당연히 순서를 맞출수도 없고) 그냥 Block내에 모든 메모리가 해제되었으면 다시 Pool에 넣어서 꺼내쓰고 하는 것. 그렇기 때문에 4프레임 정도로 생명주기를 설정하고 있다. 이게 꽉 차면 듀얼 스레드 할당자로 넘어간다.
Dual Thread Allocator
이건 그냥 Cpp의 힙 할당 같은 거다. 작은 애들은 Bucket 할당자라는 걸 써서 메모리 파편화를 줄인다. 용도(Main, Gfx)별로 할당자가 다르기도 함. Allocator.Persistent가 이걸 쓴다.
기타 Allocator
- Allocator.Invalid : 직접 쓸 일은 없다. 그냥 만들어놓고 아무것도 할당 안 하면 Invalid상태이다.
public class SomeManager : MonoBehaviour
{
NativeArray<float> _loadedData;
void LoadData(bool loadMore)
{
if (_loadedData.IsCreated)
_loadedData.Dispose();
if (loadMore)
{
_sensorData = new NativeArray<float>(1000, Allocator.Persistent);
}
else
{
_sensorData = new NativeArray<float>(100, Allocator.Persistent);
}
}
}
- Allocator.None : None은 좀 특이한 케이스인데, NativeContainer의 View따위를 제공할 때 쓴다.
NativeArray<int> original = new NativeArray<int>(100, Allocator.Persistent);
//이걸 dispose한다고 원본이 해제되진 않는다.
NativeSlice<int> slice = new NativeSlice<int>(original, 10, 20);
혹은 Persistent인데 안전 검사 하지 않을때도 쓴다고 함.
- Allocator.AudioKernel : 오디오 전용. 저 지연(latency)을 위한 오디오에 최적화된 할당자.
- 구조체형 할당자 (RewindableAllocator 등) : 커스텀 할당자인데, 가령 RewindableAllocator는 TempJob보다는 길지만 그렇다고 Persistent가 아닌 특정 구간동안만 메모리를 쓰고 한번에 비워버리고 싶을 때 쓴다.
참고자료
https://docs.unity3d.com/Packages/com.unity.collections@2.1/manual/allocator-overview.html
https://docs.unity3d.com/Manual/performance-native-allocators.html
'게임엔진 > 유니티' 카테고리의 다른 글
| IL2CPP가 Virtual Call과 Boxing을 처리하는 방법 (0) | 2023.11.19 |
|---|---|
| (협업) Unity Accelerator - 임포트 시간 단축 (0) | 2023.06.10 |
| Update() (0) | 2023.06.04 |
| (토막상식) NativeContainer (1) | 2023.05.28 |
| (토막상식) 유니티 메모리 (0) | 2023.05.21 |