GC 할당을 줄이고 빠른 임시 버퍼가 필요할 때, C#의 Span<T>와 stackalloc는 강력한 선택지입니다. Span<T>는 연속된 메모리를 안전하게 다루는 ref struct이며, stackalloc는 스택에 고정 크기 버퍼를 즉시 생성합니다. 두 기능을 함께 사용하면 작은 임시 버퍼 작업에서 힙 할당 없이 매우 빠른 처리가 가능합니다.
1. Span<T>와 stackalloc를 왜 써야 하나요?
짧게 쓰고 버리는 임시 버퍼를 힙에 만들면 GC 부담이 커집니다. stackalloc는 스택에 메모리를 확보하므로 함수가 끝나면 즉시 회수되며, Span<T>는 해당 메모리를 안전하게 다룹니다. 문자열 파싱, 바이너리 변환, 포맷팅, 스트림 읽기 등에서 특히 효과적입니다.
2. 기본 사용법: stackalloc로 Span 만들기
stackalloc는 unsafe 없이 Span<T>로 직접 받을 수 있습니다.
using System;
class Demo
{
static void Basic()
{
Span buffer = stackalloc byte[256];
for (int i = 0; i < buffer.Length; i++)
buffer[i] = (byte)i;
// 일부만 잘라 쓰기
Span slice = buffer.Slice(10, 20);
slice.Clear(); // 10~29 구간을 0으로 설정
// 주의: Span은 ref struct라서 반환/박싱/필드 저장 불가
}
}
3. 힙 할당 없이 숫자 포맷팅하기
TryFormat과 stackalloc를 조합하면 숫자를 문자열로 만들 때 중간 할당을 피할 수 있습니다.
using System;
using System.Globalization;
static string IntToStringNoAlloc(int value)
{
Span buf = stackalloc char[32]; // int 최대 길이에 충분
if (value.TryFormat(buf, out int written, provider: CultureInfo.InvariantCulture))
{
return new string(buf.Slice(0, written));
}
return value.ToString(CultureInfo.InvariantCulture);
}
4. 빠른 헥스(HEX) 인코딩 예시
바이트 배열을 헥스 문자열로 변환할 때도 stackalloc를 활용하면 매우 빠릅니다.
using System;
static string ToHex(ReadOnlySpan data)
{
Span chars = stackalloc char[data.Length * 2];
int p = 0;
foreach (byte b in data)
{
chars[p++] = NibbleToHex(b >> 4);
chars[p++] = NibbleToHex(b & 0xF);
}
return new string(chars);
static char NibbleToHex(int v) => (char)(v < 10 ? '0' + v : 'a' + (v - 10));
}
5. 문자열 파싱: ReadOnlySpan<char> 활용
문자열 조작도 AsSpan과 Slice로 부분 문자열을 할당 없이 다룰 수 있습니다. .NET에는 TryParse 오버로드가 ReadOnlySpan<char>를 지원합니다.
using System;
using System.Globalization;
static int ParseFirstNumber(ReadOnlySpan line)
{
int comma = line.IndexOf(',');
ReadOnlySpan first = comma >= 0 ? line.Slice(0, comma) : line;
if (int.TryParse(first, NumberStyles.Integer, CultureInfo.InvariantCulture, out int value))
return value;
return 0;
}
// 사용 예시
// var n = ParseFirstNumber("123,456,789".AsSpan());
6. Stream 읽기에서의 스택 버퍼
짧은 블록을 반복해서 읽는 경우 스택 버퍼는 매우 효율적입니다. Stream.Read(Span<byte>) 오버로드를 활용하세요.
using System;
using System.IO;
static void ReadSmallBlocks(string path)
{
using var fs = File.OpenRead(path);
Span buffer = stackalloc byte[4096];
int read;
while ((read = fs.Read(buffer)) > 0)
{
ReadOnlySpan chunk = buffer.Slice(0, read);
// chunk 처리
}
}
7. 크기 가변 시: stackalloc + ArrayPool 하이브리드
버퍼 크기가 작을 때는 stackalloc, 클 때는 ArrayPool을 쓰는 하이브리드 패턴이 좋습니다. 스택 오버플로우 위험을 줄이면서 GC 부담도 완화합니다.
using System;
using System.Buffers;
static void UseHybridBuffer(int size)
{
const int StackThreshold = 1024; // 상황에 맞게 조정
byte[]? rented = null;
Span buffer = size <= StackThreshold ? stackalloc byte[size] : default;
if (buffer == default)
{
rented = ArrayPool.Shared.Rent(size);
buffer = rented.AsSpan(0, size);
}
try
{
buffer.Clear();
// buffer 사용
}
finally
{
if (rented != null)
ArrayPool.Shared.Return(rented);
}
}
8. 제약과 주의사항
Span<T>는 ref struct이므로 다음 제약을 반드시 지켜야 합니다.
- 힙에 저장 불가: 클래스 필드, 박싱, 인터페이스 변환 불가입니다.
- 비동기/이터레이터 경계 불가: await 또는 yield를 넘어서 생존할 수 없습니다.
- 람다 캡처 불가: 클로저로 캡처할 수 없습니다.
- 수명 준수: stackalloc로 만든 Span은 현재 메서드/스코프를 벗어나면 무효입니다.
또한 stackalloc 크기를 과도하게 키우면 StackOverflowException 위험이 있습니다. 일반적으로 수백~수천 바이트 정도의 작은 버퍼에 한정하는 것이 좋습니다.
9. 성능 팁 요약
- 작은 임시 버퍼: stackalloc + Span 활용으로 힙 할당 제거
- 큰/가변 버퍼: ArrayPool과 혼용
- 문자열 처리: AsSpan, Slice, TryParse/TryFormat 적극 활용
- 안전성: Span 수명은 스코프 내로 제한, async 경계 넘지 않기
- 측정: BenchmarkDotNet 등으로 실제 워크로드에서 할당과 시간 모두 확인
정리하면, Span<T>와 stackalloc는 작은 임시 버퍼를 빠르게 처리하고 GC 압력을 줄이는 데 최적화된 도구입니다. 위 패턴들을 적절히 적용해 핫패스의 할당을 제거하고 전체 처리량과 지연 시간을 개선해 보시기 바랍니다.
'C#' 카테고리의 다른 글
| C# Stopwatch로 함수 호출 빈도 측정하기 (0) | 2026.07.12 |
|---|---|
| C# 직렬화 시 참조 순환 처리 전략 (0) | 2026.07.12 |
| C# 파일 시스템 감시(FileSystemWatcher)로 이벤트 처리 (0) | 2026.07.10 |
| C# BlockingCollection<T>로 생산자-소비자 패턴 구현 (0) | 2026.07.09 |
| C# ThreadPool Worker Thread 관리 최적화 (0) | 2026.07.09 |