C# 7.2+의 Span과 Memory

C# 7.2 이후의 Span와 Memory

이 타입들에 관심을 가져야 하는 이유

Span<T>(C# 7.2에서 도입)와 형제 격인 Memory<T>는 추가 할당이나 복사 없이 연속된 데이터 블록을 다룰 수 있게 해준다. 그러면서도 C#의 안전성 규칙 안에 머무른다. 스택, 관리 힙, 심지어 비관리 버퍼까지 가리킬 수 있는 비용 없는 슬라이스형 메모리 뷰라고 생각하면 된다.

대표적인 용도:

  • 할당 하나하나가 부담되는 고빈도 텍스트/바이너리 파싱
  • 더 작은 조각으로 잘라 써야 하는 대용량 스트리밍 버퍼 (파일, 소켓, 이미지)
  • GC 일시 정지를 최소로 유지해야 하는 성능 critical 경로

Span<T> 한눈에 보기

특징 의미
값 타입 (ref struct) 스택에 존재한다. 넘겨주는 비용이 거의 없다.
스택 전용 현재 메서드를 벗어날 수 없어서, JIT가 수명이 끝나는 시점을 정확히 안다.
범용 뷰 같은 API로 배열, stackalloc 블록, 비관리 메모리를 모두 슬라이스한다.

최소 예제

int[] data = { 1, 2, 3, 4, 5 };

Span<int> span   = data;        // 배열 전체
Span<int> middle = span[1..4];  // 1~3번 원소

middle[0] = 99;                 // data[1]을 바꾼다
Console.WriteLine(string.Join(", ", data));
// 1, 99, 3, 4, 5

슬라이스는 그저 가벼운 뷰일 뿐이고, 원소는 하나도 복사되지 않았다.

stackalloc 사용

Span<byte> buffer = stackalloc byte[256];
for (int i = 0; i < buffer.Length; i++)
    buffer[i] = (byte)i;

스택에 256바이트를 확보한다. GC 부담이 전혀 없고, 메서드가 반환될 때 자동으로 정리된다.

Memory<T> 등장

Span<T>는 현재 스택 프레임을 넘어 살아남거나 await를 가로지를 수 없다. 그런 유연성이 필요하면 Memory<T>(또는 읽기 전용 짝인 ReadOnlyMemory<T>)로 바꾼다.

항목 Span<T> Memory<T>
저장 위치 스택 전용 힙 또는 스택
await / 이터레이터 가로지르기 ✘ ✔
가변성 읽기/쓰기 읽기/쓰기 (ReadOnlyMemory<T>는 읽기 전용)

비동기 친화적인 예제

public async Task ParseAsync(ReadOnlyMemory<byte> packet)
{
    var header = packet[..4];
    var body   = packet[4..];

    await Task.Yield();          // 안전하다: Memory는 await를 넘어 살아남는다

    Console.WriteLine(
        $"Header={BitConverter.ToUInt32(header.Span)} Body bytes={body.Length}");
}

동기 코드 블록 안에서는 여전히 .Span을 호출해 빠른 스택 전용 뷰를 얻을 수 있다.

실전 팁

  • 입력 매개변수에는 ReadOnlySpan<T>를 선호하라. 의도가 분명해지고 실수로 쓰는 것을 막아준다.
  • 복사 대신 슬라이스를 이어 붙여라. 모든 슬라이스([..] 또는 Slice)는 O(1)이다.
  • Span<T>는 할당 없는 변환을 위한 TryParse / TryFormat API와 자연스럽게 짝을 이룬다.
  • 데이터를 저장해야 하거나 await 이후까지 유지해야 한다면, 일찍 Memory<T>로 올려두고 짧은 동기 작업에서만 .Span으로 되돌린다.

주의점과 함정

  1. 탈출 규칙 컴파일러는 Span<T>를 현재 메서드보다 오래 살아남을 수 있는 코드에 넘기는 것을 금지한다. 그 에러들을 존중하자. use-after-free 버그를 막아준다.
  2. 힙 단편화 작은 Memory<T> 인스턴스를 오래 사는 필드로 잔뜩 들고 있으면 LOH가 단편화될 수 있다. 가능하면 풀링하거나 재사용하자.
  3. 플랫폼 지원 .NET Core 2.1+, .NET 5+, 또는 .NET Standard 2.1이 필요하다. 오래된 Unity나 Xamarin 버전은 완전한 지원이 없을 수 있다.
  4. 비관리 데이터 슬라이싱 MemoryMarshal.CreateSpan으로 raw 포인터를 감쌀 수 있지만, 수명은 직접 관리해야 하고 pinning 비용에도 주의해야 한다.