콘텐츠로 이동

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최소빠름재사용 가능 중형 버퍼

using System.Runtime.InteropServices;
// 네이티브 ABI에 맞는 구조체 레이아웃
[StructLayout(LayoutKind.Sequential)]
public struct POINT
{
public int X;
public int Y;
}
[StructLayout(LayoutKind.Explicit)]
public struct UNION_EXAMPLE
{
[FieldOffset(0)] public int AsInt;
[FieldOffset(0)] public float AsFloat; // int와 메모리 공유
}
// P/Invoke 호출
[LibraryImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
private static partial bool GetCursorPos(out POINT point);
// 관리 코드에서 호출
GetCursorPos(out POINT pos);
Console.WriteLine($"커서: ({pos.X}, {pos.Y})");

// SafeHandle로 안전한 비관리 자원 래핑
public sealed class SafeNativeBuffer : SafeHandle
{
public SafeNativeBuffer(int size) : base(IntPtr.Zero, true)
{
unsafe
{
SetHandle((IntPtr)NativeMemory.Alloc((nuint)size));
}
}
public override bool IsInvalid => handle == IntPtr.Zero;
protected override bool ReleaseHandle()
{
unsafe
{
NativeMemory.Free((void*)handle);
}
return true;
}
public unsafe Span<byte> AsSpan(int length) =>
new Span<byte>((void*)handle, length);
}
// 사용 — SafeHandle이 GC 파이널라이저에서 자동 해제
using var buffer = new SafeNativeBuffer(4096);
buffer.AsSpan(4096).Fill(0xFF);

.NET 6+ 환경에서 대용량 버퍼나 SIMD 연산 데이터는 NativeMemory.AlignedAllocSpan<T>으로 GC 힙 바깥에서 관리하세요. 1KB 이하의 임시 버퍼는 stackalloc이 가장 빠릅니다. MemoryMarshal.CastUnsafe.As로 복사 없는 타입 재해석이 가능하며, 모든 비관리 할당은 IDisposable 패턴이나 SafeHandle로 수명을 보장하세요. P/Invoke 연동 시 [StructLayout]으로 메모리 레이아웃을 명시하고, LibraryImport(Source Generator 기반)를 사용하면 AOT 호환 코드가 생성됩니다.