C# unsafe 코드와 포인터 활용
C#은 기본적으로 메모리 안전한 관리 코드를 실행하지만, unsafe 키워드를 사용하면 C/C++ 수준의 포인터 연산이 가능합니다. 고성능 I/O, 인터롭, 이미지 처리 등 특수한 시나리오에서 필수적인 도구입니다.
1. unsafe 활성화
섹션 제목: “1. unsafe 활성화”<PropertyGroup> <AllowUnsafeBlocks>true</AllowUnsafeBlocks></PropertyGroup>// 메서드 수준public static unsafe void ProcessData(byte* ptr, int length) { }
// 클래스 수준public unsafe class NativeBuffer { }
// 블록 수준public void Example(){ unsafe { int x = 42; int* p = &x; *p = 100; Console.WriteLine(x); // 100 }}2. 기본 포인터 연산
섹션 제목: “2. 기본 포인터 연산”unsafe{ int[] arr = { 10, 20, 30, 40, 50 };
fixed (int* p = arr) // GC가 배열을 이동하지 못하도록 고정 { // 포인터 산술 for (int i = 0; i < arr.Length; i++) { Console.WriteLine(*(p + i)); // 10, 20, 30, 40, 50 }
// 인덱서 문법 Console.WriteLine(p[2]); // 30
// 포인터 증가 int* p2 = p; p2++; // 4바이트 이동 (int 크기) Console.WriteLine(*p2); // 20 }}3. fixed 문
섹션 제목: “3. fixed 문”GC(Garbage Collector)는 메모리 압축 시 객체를 이동시킵니다. 포인터를 사용하는 동안 GC가 객체를 이동하면 포인터가 무효화됩니다. fixed 문은 해당 객체를 일시적으로 고정합니다.
class ImageProcessor{ private byte[] _buffer = new byte[1024 * 1024];
public unsafe void Fill(byte value) { fixed (byte* ptr = _buffer) { byte* p = ptr; byte* end = ptr + _buffer.Length;
// 8바이트씩 처리 (unrolled loop) while (p + 8 <= end) { *(ulong*)p = 0x0101010101010101UL * value; p += 8; }
// 나머지 while (p < end) *p++ = value; } // fixed 블록 종료 후 고정 해제 }
// string 고정 public unsafe void ProcessString(string s) { fixed (char* pStr = s) { char* p = pStr; while (*p != '\0') { // 각 문자 처리 p++; } } }}4. stackalloc — 스택 메모리 할당
섹션 제목: “4. stackalloc — 스택 메모리 할당”힙 대신 스택에 메모리를 할당합니다. 블록 종료 시 자동 해제되며 GC 압력이 없습니다.
// unsafe 없이도 Span<T>과 함께 사용 가능 (C# 7.3+)public static int SumSmall(ReadOnlySpan<int> data){ Span<int> temp = stackalloc int[data.Length]; // 힙 할당 없음 data.CopyTo(temp);
// 정렬 후 합산 temp.Sort(); int sum = 0; foreach (int v in temp) sum += v; return sum;}
// unsafe 버전public static unsafe int FastSum(int* data, int n){ int* temp = stackalloc int[n]; // 스택에 n*4바이트 할당 for (int i = 0; i < n; i++) temp[i] = data[i]; // 블록 종료 시 자동 해제 (free 불필요)
int sum = 0; for (int i = 0; i < n; i++) sum += temp[i]; return sum;}주의: 스택 크기는 보통 1MB~8MB입니다. stackalloc으로 큰 배열을 할당하면 스택 오버플로우가 발생합니다.
5. 구조체 포인터
섹션 제목: “5. 구조체 포인터”struct Vector3{ public float X, Y, Z;}
unsafe{ Vector3 v = new Vector3 { X = 1.0f, Y = 2.0f, Z = 3.0f }; Vector3* p = &v;
p->X = 10.0f; // 포인터로 멤버 접근 Console.WriteLine(v.X); // 10
// 배열 of struct Vector3[] vectors = new Vector3[100]; fixed (Vector3* pv = vectors) { for (int i = 0; i < 100; i++) { pv[i].X = i; pv[i].Y = i * 2.0f; pv[i].Z = i * 3.0f; } }}6. 포인터와 Span<T> 변환
섹션 제목: “6. 포인터와 Span<T> 변환”using System.Runtime.InteropServices;
public static unsafe void Example(){ byte[] buffer = new byte[256];
fixed (byte* ptr = buffer) { // 포인터 → Span (unsafe 없이 사용 가능) Span<byte> span = new Span<byte>(ptr, buffer.Length); span.Fill(0xFF); }
// MemoryMarshal을 통한 변환 Span<byte> byteSpan = buffer.AsSpan(); Span<int> intSpan = MemoryMarshal.Cast<byte, int>(byteSpan); // 256 bytes → 64 ints}7. 비관리 코드 인터롭 (P/Invoke)
섹션 제목: “7. 비관리 코드 인터롭 (P/Invoke)”using System.Runtime.InteropServices;
class NativeInterop{ [DllImport("kernel32.dll", SetLastError = true)] private static extern unsafe bool WriteFile( IntPtr hFile, byte* lpBuffer, int nNumberOfBytesToWrite, out int lpNumberOfBytesWritten, IntPtr lpOverlapped );
public static unsafe void WriteToHandle(IntPtr handle, byte[] data) { fixed (byte* pData = data) { bool success = WriteFile(handle, pData, data.Length, out int written, IntPtr.Zero);
if (!success) throw new InvalidOperationException( $"WriteFile failed: {Marshal.GetLastWin32Error()}"); } }}8. 고정 크기 버퍼 (Fixed-size Buffer)
섹션 제목: “8. 고정 크기 버퍼 (Fixed-size Buffer)”// 관리 구조체 안에 인라인 배열 (C 스타일 struct 모방)unsafe struct PacketHeader{ public fixed byte Magic[4]; // 4바이트 인라인 배열 public int Length; public ushort Checksum; public fixed byte Reserved[2];}
unsafe{ PacketHeader header; header.Magic[0] = 0xDE; header.Magic[1] = 0xAD; header.Magic[2] = 0xBE; header.Magic[3] = 0xEF; header.Length = 1024;}9. unsafe vs Span<T> 선택 기준
섹션 제목: “9. unsafe vs Span<T> 선택 기준”| 상황 | 권장 |
|---|---|
| 일반 고성능 코드 | Span<T>, Memory<T> |
| 외부 네이티브 라이브러리 연동 | unsafe + P/Invoke |
| 레거시 비관리 API | unsafe |
| 스택 임시 버퍼 | stackalloc + Span<T> |
| 구조체 내 인라인 배열 | fixed 버퍼 |
unsafe 코드는 강력하지만 GC 안전성, 타입 안전성, 메모리 안전성을 모두 프로그래머가 직접 보장해야 합니다. .NET 6+ 환경에서는 Span<T>, Memory<T>, MemoryMarshal API가 대부분의 고성능 시나리오를 unsafe 없이 처리할 수 있으므로, 반드시 필요한 경우(네이티브 인터롭, 특수 최적화)에만 unsafe를 사용하세요.