콘텐츠로 이동

C# Span<T> & Memory<T> 활용

전통적인 배열 슬라이싱은 새로운 배열을 할당합니다.

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

읽기 전용 뷰를 제공합니다. 문자열 처리에 특히 유용합니다.

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")); // 150

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)); // 슬라이스

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)
{
// 실제 처리
}

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);
ArraySegmentSpanMemory
힙 저장OXO
async 사용OXO
스택 할당XOX
성능보통최고높음
비관리 메모리XOO

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 압력을 크게 줄일 수 있습니다.