C# NativeMemory와 비관리 메모리 직접 제어
C# 6.0 이후의 System.Runtime.InteropServices.NativeMemory(NET 6+), Unsafe, MemoryMarshal은 GC 힙 외부의 비관리 메모리를 직접 다루는 API를 제공합니다. 대용량 버퍼, 고성능 직렬화, 네이티브 라이브러리 연동에 필수입니다.
1. NativeMemory — 비관리 메모리 할당
섹션 제목: “1. NativeMemory — 비관리 메모리 할당”using System.Runtime.InteropServices;
// 정렬된 비관리 메모리 할당void* ptr = NativeMemory.AlignedAlloc( byteCount: 1024, alignment: 64); // 64바이트(캐시 라인) 정렬
try{ // Span으로 래핑해 안전하게 접근 var span = new Span<float>(ptr, 256); span.Fill(0f); span[0] = 3.14f;}finally{ NativeMemory.AlignedFree(ptr);}
// 크기 조정void* resized = NativeMemory.Realloc(ptr, 2048);2. stackalloc — 스택 할당
섹션 제목: “2. stackalloc — 스택 할당”// 소형 임시 버퍼: 스택에 할당 (GC 없음)Span<byte> buffer = stackalloc byte[256];buffer.Fill(0);buffer[0] = 0xFF;
// 조건부 스택/힙 할당 패턴const int StackThreshold = 512;int size = GetRequiredSize();
Span<byte> buf = size <= StackThreshold ? stackalloc byte[size] : new byte[size]; // 힙 할당
ProcessData(buf);// 스택 할당은 scope 종료 시 자동 해제, 힙은 GC3. Unsafe 클래스 — 비관리 포인터 연산
섹션 제목: “3. Unsafe 클래스 — 비관리 포인터 연산”using System.Runtime.CompilerServices;
// 포인터 없이 오프셋 접근byte[] arr = new byte[100];ref byte start = ref arr[0];ref byte at10 = ref Unsafe.Add(ref start, 10);at10 = 0xFF;
// 크기 쿼리int size = Unsafe.SizeOf<Vector3>(); // 12
// 타입 재해석 (비트캐스트 유사)int intVal = 0x3F800000;float floatVal = Unsafe.As<int, float>(ref intVal); // 1.0f
// 포인터 고정 없이 관리 객체 참조byte[] data = new byte[10];ref byte first = ref MemoryMarshal.GetArrayDataReference(data);4. MemoryMarshal — 메모리 재해석
섹션 제목: “4. MemoryMarshal — 메모리 재해석”using System.Runtime.InteropServices;
// byte[] → float[] 재해석 (복사 없음)byte[] bytes = new byte[16];Span<float> floats = MemoryMarshal.Cast<byte, float>(bytes);// floats.Length == 4
// 구조체 ↔ byte 변환Vector3 v = new Vector3(1, 2, 3);Span<byte> raw = MemoryMarshal.AsBytes( MemoryMarshal.CreateSpan(ref v, 1));// raw.Length == 12
// 비관리 메모리를 Span으로unsafe{ byte* ptr = stackalloc byte[64]; Span<byte> span = new Span<byte>(ptr, 64); span.Clear();}5. 비관리 타입 배열 — UnmanagedArray
섹션 제목: “5. 비관리 타입 배열 — UnmanagedArray”// NativeMemory를 IDisposable로 래핑하는 패턴unsafe sealed class NativeArray<T> : IDisposable where T : unmanaged{ private T* _ptr; public int Length { get; }
public NativeArray(int length) { Length = length; _ptr = (T*)NativeMemory.Alloc( (nuint)(length * sizeof(T))); }
public ref T this[int i] => ref _ptr[i];
public Span<T> AsSpan() => new Span<T>(_ptr, Length);
public void Dispose() { if (_ptr != null) { NativeMemory.Free(_ptr); _ptr = null; } }}
// 사용using var arr = new NativeArray<float>(1024);arr.AsSpan().Fill(1.0f);arr[0] = 999f;6. GC 핀닝 — fixed 키워드
섹션 제목: “6. GC 핀닝 — fixed 키워드”byte[] managed = new byte[256];
unsafe{ fixed (byte* ptr = managed) { // GC가 managed 배열을 이동하지 않음 // 네이티브 API에 포인터 전달 가능 NativeApi.Process(ptr, managed.Length); } // fixed 블록 종료 → 핀 해제}
// GCHandle을 사용한 장기 핀닝var handle = GCHandle.Alloc(managed, GCHandleType.Pinned);IntPtr address = handle.AddrOfPinnedObject();// 사용 후 반드시 해제handle.Free();7. 성능 비교
섹션 제목: “7. 성능 비교”| 할당 방식 | GC 부담 | 속도 | 사용 상황 |
|---|---|---|---|
new T[] | 있음 | 빠름 | 일반 용도 |
stackalloc | 없음 | 최고 | 소형 임시 버퍼 (<1KB) |
NativeMemory.Alloc | 없음 | 빠름 | 대형 비관리 버퍼 |
ArrayPool<T>.Rent | 최소 | 빠름 | 재사용 가능 중형 버퍼 |
NET 6+ 환경에서 대용량 버퍼나 SIMD 연산 데이터는 NativeMemory.AlignedAlloc과 Span<T>으로 GC 힙 바깥에서 관리하세요. 1KB 이하의 임시 버퍼는 stackalloc이 가장 빠릅니다. MemoryMarshal.Cast와 Unsafe.As로 복사 없는 타입 재해석이 가능하며, 모든 비관리 할당은 IDisposable 패턴으로 수명을 보장하세요.