← 문제로

5. Span<T>/stackalloc 와 ref struct 의 스택 전용 제약

난이도 상
내 리뷰 · C#
해설 · C#

해설 — Span/stackalloc 와 ref struct 의 스택 전용 제약

난이도: 상

요약

Span<T>ref struct라 항상 스택에만 존재할 수 있다. 그래서 힙 메모리/스택 메모리/배열의 일부를 복사 없이 가리키는 뷰로 안전하게 쓸 수 있다. 대신 힙으로 "탈출"하는 모든 경로(박싱, 클래스 필드, 클로저 캡처, await/yield 경계를 넘는 지역 변수)가 컴파일 단계에서 금지된다. 이 제약이 곧 "가리키는 메모리가 사라진 뒤에도 살아있는 Span"이라는 위험을 원천 차단한다.

컴파일 가부 예측

(F1) object o = (object)span;        → 컴파일 오류  (ref struct 박싱 불가)
(F2) class Box { Span<byte> f; }      → 컴파일 오류  (ref struct 를 일반 클래스 필드로 불가)
(F3) Func<int> f = () => span[0];     → 컴파일 오류  (ref struct 를 클로저로 캡처 불가)
(F4) await 뒤에서 Span 지역변수 사용   → 컴파일 오류  (await 경계를 넘는 ref struct 지역변수 불가)
(F5) await 없는 동기 블록에서 Span 사용 → 컴파일 OK   (스택 프레임 안에서만 사용)

핵심 개념

  • ref struct = 스택 전용: Span<T>/ReadOnlySpan<T>는 ref struct. 인스턴스가 힙에 올라갈 수 없다. 따라서 박싱 불가, 클래스/일반 struct의 필드 불가, 배열 원소 불가, 제네릭 타입 인자 불가(C# 12 이전), 클로저/이터레이터/async 상태머신에 캡처 불가.
  • 스택메모리 + 뷰: stackalloc byte[n]은 현재 스택 프레임에 메모리를 잡고 Span<byte>로 노출한다(GC 무관, 메서드 반환 시 소멸). Span은 "포인터 + 길이"라 Slice복사 없이 같은 메모리의 부분 뷰를 만든다.
  • 왜 안전한가: ref struct가 힙으로 못 가니, "Span이 가리키는 스택 메모리/배열보다 Span이 더 오래 살 수 없다"가 컴파일러에 의해 보장된다 → dangling 뷰가 불가능.

단계별 내부 동작 / 조각 설명

  1. ParseHeader 무할당: ReadOnlySpan<byte> buf는 호출자의 버퍼(배열/스택/네이티브)를 가리키는 뷰. BinaryPrimitives.Read...는 그 뷰에서 직접 정수를 읽고, buf.Slice(4, len)payload를 복사하지 않고 같은 메모리의 부분 뷰를 돌려준다. 새 배열 할당이 없다 → GC 압박 0.
  2. (F1) 박싱: ref struct를 object로 캐스팅하면 힙 박스가 필요 → 금지. 컴파일 오류.
  3. (F2) 클래스 필드: 클래스 인스턴스는 힙에 있으므로 그 필드인 Span도 힙에 실린다 → 금지. (단, 다른 ref struct의 필드로는 가능.)
  4. (F3) 클로저 캡처: 람다가 캡처한 지역은 컴파일러가 힙 클래스(디스플레이 클래스) 로 끌어올린다 → ref struct가 힙으로 탈출 → 금지. 로컬 함수도 캡처하면 동일.
  5. (F4) async 경계: await가 있으면 메서드의 지역변수는 힙의 상태머신 객체에 저장되어 재개를 견딘다 → ref struct는 거기 못 들어간다. 그래서 Span 지역변수가 await를 가로질러 살아있으면 컴파일 오류. (await 이전에 다 쓰고 끝내면 OK.)
  6. (F5) 동기 블록: 스택 프레임을 벗어나지 않으므로 정상. ref struct의 정상적인 사용처.

흔한 오해·함정

  • "Span을 필드에 저장해 두고 나중에 쓰자" → 불가능(클래스 필드 금지). 객체에 보관하려면 Memory<T>/ReadOnlyMemory<T> (이건 일반 struct라 힙·async 가능, 대신 .Span으로 매번 펼침).
  • "async 메서드에서 stackalloc/Span으로 무할당 파싱" → await를 넘기면 못 쓴다. 비동기 경로는 Memory<T> + 동기 구간에서만 .Span 사용.
  • stackalloc 크기를 입력값으로 크게 잡으면 스택 오버플로 → 작은 상한에서만, 크면 ArrayPool<T>.
  • ref struct가 아니었다면(Span이 일반 struct/class였다면), 스택 버퍼나 해제된 배열을 가리키는 Span을 힙/필드/다른 스레드로 넘겨 use-after-free/메모리 안전성 붕괴가 가능했을 것 — 그래서 ref struct 제약이 본질이다.

면접 포인트

  1. Span<T>ref struct(스택 전용) — 박싱/클래스 필드/클로저/await 경계 탈출 불가. 이 제약이 "복사 없는 뷰"의 메모리 안전성을 컴파일 타임에 보장한다.
  2. 무할당 파싱: stackalloc/배열을 Span으로 보고 Slice로 복사 없이 부분 뷰. 게임 서버 패킷 처리에 핵심.
  3. 비동기에서 보관·전달이 필요하면 Memory<T>, 실제 접근 시 동기 구간에서 .Span. stackalloc은 작은 크기에서만(스택 오버플로 주의).