콘텐츠로 이동

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);

// 소형 임시 버퍼: 스택에 할당 (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 종료 시 자동 해제, 힙은 GC

3. 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);

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;

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();

할당 방식GC 부담속도사용 상황
new T[]있음빠름일반 용도
stackalloc없음최고소형 임시 버퍼 (<1KB)
NativeMemory.Alloc없음빠름대형 비관리 버퍼
ArrayPool<T>.Rent최소빠름재사용 가능 중형 버퍼

NET 6+ 환경에서 대용량 버퍼나 SIMD 연산 데이터는 NativeMemory.AlignedAllocSpan<T>으로 GC 힙 바깥에서 관리하세요. 1KB 이하의 임시 버퍼는 stackalloc이 가장 빠릅니다. MemoryMarshal.CastUnsafe.As로 복사 없는 타입 재해석이 가능하며, 모든 비관리 할당은 IDisposable 패턴으로 수명을 보장하세요.