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 뷰가 불가능.
단계별 내부 동작 / 조각 설명
- ParseHeader 무할당:
ReadOnlySpan<byte> buf는 호출자의 버퍼(배열/스택/네이티브)를 가리키는 뷰.BinaryPrimitives.Read...는 그 뷰에서 직접 정수를 읽고,buf.Slice(4, len)은 payload를 복사하지 않고 같은 메모리의 부분 뷰를 돌려준다. 새 배열 할당이 없다 → GC 압박 0. - (F1) 박싱: ref struct를
object로 캐스팅하면 힙 박스가 필요 → 금지. 컴파일 오류. - (F2) 클래스 필드: 클래스 인스턴스는 힙에 있으므로 그 필드인 Span도 힙에 실린다 → 금지. (단, 다른 ref struct의 필드로는 가능.)
- (F3) 클로저 캡처: 람다가 캡처한 지역은 컴파일러가 힙 클래스(디스플레이 클래스) 로 끌어올린다 → ref struct가 힙으로 탈출 → 금지. 로컬 함수도 캡처하면 동일.
- (F4) async 경계:
await가 있으면 메서드의 지역변수는 힙의 상태머신 객체에 저장되어 재개를 견딘다 → ref struct는 거기 못 들어간다. 그래서 Span 지역변수가await를 가로질러 살아있으면 컴파일 오류. (await 이전에 다 쓰고 끝내면 OK.) - (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 제약이 본질이다.
면접 포인트
Span<T>는 ref struct(스택 전용) — 박싱/클래스 필드/클로저/await경계 탈출 불가. 이 제약이 "복사 없는 뷰"의 메모리 안전성을 컴파일 타임에 보장한다.- 무할당 파싱:
stackalloc/배열을Span으로 보고Slice로 복사 없이 부분 뷰. 게임 서버 패킷 처리에 핵심. - 비동기에서 보관·전달이 필요하면
Memory<T>, 실제 접근 시 동기 구간에서.Span.stackalloc은 작은 크기에서만(스택 오버플로 주의).