C# Span<T> & Memory<T> 활용
왜 Span가 필요한가
섹션 제목: “왜 Span가 필요한가”전통적인 배열 슬라이싱은 새로운 배열을 할당합니다.
byte[] data = new byte[1024];
// 나쁜 예: 새 배열 할당 → GC 압력byte[] slice = data[100..200];
// Span<T>: 할당 없이 원본 메모리의 뷰만 생성Span<byte> span = data.AsSpan(100, 100);Span<T>는 스택에만 존재하는(ref struct) 메모리 뷰입니다. 힙 할당 없이 연속 메모리 블록을 안전하게 조작합니다.
Span 기본 사용법
섹션 제목: “Span 기본 사용법”// 배열에서 Span 생성int[] array = { 1, 2, 3, 4, 5 };Span<int> span = array.AsSpan();
// 슬라이싱 — 복사 없음Span<int> middle = span.Slice(1, 3); // [2, 3, 4]// 또는 범위 구문Span<int> same = span[1..4];
// 수정은 원본 배열에 반영됨middle[0] = 99;Console.WriteLine(array[1]); // 99
// 스택 할당 메모리Span<int> stackData = stackalloc int[8];stackData.Fill(0);ReadOnlySpan
섹션 제목: “ReadOnlySpan”읽기 전용 뷰를 제공합니다. 문자열 처리에 특히 유용합니다.
string text = "Hello, World!";
// 문자열 → ReadOnlySpan (복사 없음)ReadOnlySpan<char> span = text.AsSpan();
// 슬라이싱ReadOnlySpan<char> hello = span[..5]; // "Hello"ReadOnlySpan<char> world = span[7..12]; // "World"
Console.WriteLine(hello.ToString());
// 비교bool starts = span.StartsWith("Hello".AsSpan()); // true
// 파싱 — 중간 문자열 할당 없음ReadOnlySpan<char> numStr = " 42 ".AsSpan().Trim();int parsed = int.Parse(numStr);고성능 문자열 파싱
섹션 제목: “고성능 문자열 파싱”Span<T>로 문자열을 분리할 때 중간 string 객체 생성을 피합니다.
// 전통적 방법: Split → 여러 string 객체 생성string csv = "10,20,30,40,50";string[] parts = csv.Split(','); // 힙 할당 5개
// Span 기반: 할당 최소화static int SumCsvSpan(ReadOnlySpan<char> csv){ int total = 0; while (csv.Length > 0) { int comma = csv.IndexOf(','); ReadOnlySpan<char> token = comma < 0 ? csv : csv[..comma]; total += int.Parse(token); csv = comma < 0 ? default : csv[(comma + 1)..]; } return total;}
Console.WriteLine(SumCsvSpan("10,20,30,40,50")); // 150Memory — 힙에 저장 가능한 Span
섹션 제목: “Memory — 힙에 저장 가능한 Span”Span<T>는 ref struct이므로 힙에 저장하거나 async 메서드에서 사용할 수 없습니다. Memory<T>는 이 제약을 제거합니다.
// Span<T>: async에서 사용 불가// async Task ProcessAsync(Span<byte> data) { ... } // 컴파일 오류
// Memory<T>: async에서 사용 가능async Task ProcessAsync(Memory<byte> buffer){ await Task.Delay(10); buffer.Span.Fill(0); // Memory에서 Span으로 변환}
// 사용byte[] data = new byte[256];await ProcessAsync(data.AsMemory());await ProcessAsync(data.AsMemory(64, 64)); // 슬라이스IMemoryOwner — 메모리 수명 관리
섹션 제목: “IMemoryOwner — 메모리 수명 관리”MemoryPool<T>로 풀에서 메모리를 빌려 쓴 뒤 반환합니다.
using System.Buffers;
async Task ProcessLargeDataAsync(Stream stream){ // 메모리 풀에서 4KB 블록 임대 using IMemoryOwner<byte> owner = MemoryPool<byte>.Shared.Rent(4096); Memory<byte> buffer = owner.Memory;
int bytesRead = await stream.ReadAsync(buffer); Memory<byte> data = buffer[..bytesRead];
// data 처리 ... ProcessData(data.Span);
// using 블록 종료 시 자동 반환}
void ProcessData(ReadOnlySpan<byte> data){ // 실제 처리}ArraySegment vs Span vs Memory
섹션 제목: “ArraySegment vs Span vs Memory”byte[] array = new byte[1024];
// ArraySegment: 오래된 API. 힙 할당. 제한적 연산.ArraySegment<byte> seg = new ArraySegment<byte>(array, 0, 512);
// Span<T>: 스택 전용. 빠름. async 불가.Span<byte> span = array.AsSpan(0, 512);
// Memory<T>: 힙 저장 가능. async 가능. Span보다 약간 오버헤드.Memory<byte> mem = array.AsMemory(0, 512);| ArraySegment | Span | Memory | |
|---|---|---|---|
| 힙 저장 | O | X | O |
| async 사용 | O | X | O |
| 스택 할당 | X | O | X |
| 성능 | 보통 | 최고 | 높음 |
| 비관리 메모리 | X | O | O |
비관리 메모리와 Span
섹션 제목: “비관리 메모리와 Span”using System.Runtime.InteropServices;
// 비관리 메모리 할당nint ptr = Marshal.AllocHGlobal(1024);try{ unsafe { Span<byte> span = new Span<byte>((void*)ptr, 1024); span.Fill(0); span[0] = 42; Console.WriteLine(span[0]); // 42 }}finally{ Marshal.FreeHGlobal(ptr);}실전: 바이너리 프로토콜 파서
섹션 제목: “실전: 바이너리 프로토콜 파서”// 헤더: [magic:4] [length:4] [type:2] [payload:N]static bool TryParsePacket(ReadOnlySpan<byte> data, out int payloadLength){ payloadLength = 0; if (data.Length < 10) return false;
// 매직 넘버 확인 if (!data[..4].SequenceEqual("PACK"u8)) return false;
// 리틀 엔디안 정수 읽기 — 할당 없음 payloadLength = BitConverter.ToInt32(data.Slice(4, 4)); short type = BitConverter.ToInt16(data.Slice(8, 2));
return payloadLength > 0 && payloadLength <= data.Length - 10;}| 타입 | 용도 |
|---|---|
Span<T> | 동기 코드, 스택 할당 슬라이싱, 최고 성능 |
ReadOnlySpan<T> | 읽기 전용 메모리 뷰, 문자열 파싱 |
Memory<T> | 비동기 코드, 힙 저장이 필요한 슬라이싱 |
IMemoryOwner<T> | 메모리 풀 기반 수명 관리 |
Span<T>와 Memory<T>는 할당 제로(zero-allocation) 프로그래밍의 핵심입니다. 네트워크 파싱, 파일 처리, 게임 물리 연산처럼 성능이 중요한 경로에서 GC 압력을 크게 줄일 수 있습니다.