【 1. 배경: 왜 Span<T>와 Memory<T>인가 】
1.1. 도입 배경
C#에서 Span<T>와 Memory<T>는 고성능 메모리 관리와 메모리 안전성을 동시에 제공하기 위해 설계된 기능입니다.
- 기존에는 배열(Array)과 문자열(String)을 조작할 때 메모리 할당 및 복사가 빈번히 발생하여 성능 문제가 있었습니다.
- 또한 비동기 작업에서의 데이터 참조 문제와 Garbage Collector(GC) 처리 부담을 줄이는 데 한계가 있었습니다.
1.2. 주요 목적
- 메모리 할당 최소화: 기존의 데이터 복사 작업을 줄이고 직접 참조.
- GC 부담 감소: 할당된 메모리를 줄이고 GC의 작업량 감소.
- 안전하고 빠른 데이터 처리: 스택 또는 힙 메모리를 효율적으로 사용.
【 2. 정의와 차이점 】
2.1. Span<T>의 정의
- 정의: Span<T>는 연속적인 메모리 영역을 나타내는 구조체로, 복사 없이 데이터를 처리할 수 있습니다.
- 특징:
- 스택 기반: Span<T>는 주로 스택 메모리에서 작동하며, 메모리 복사가 없습니다.
- 값 타입: 구조체로 설계되어 메모리 오버헤드가 없습니다.
- 제약 조건: 스택 메모리에 한정되므로 비동기 작업에서는 사용할 수 없습니다.
2.2. Memory<T>의 정의
- 정의: Memory<T>는 Span<T>의 기능을 확장하여 힙 메모리에서 데이터를 참조하며, 비동기 작업에서도 사용할 수 있습니다.
- 특징:
- 힙 기반: 스택뿐만 아니라 힙 메모리에서도 작동.
- 비동기 지원: Memory<T>는 비동기 메서드와 호환 가능합니다.
- 제약 조건: Span<T>보다 약간 느리며, 약간의 오버헤드가 발생.
2.3. 주요 차이점
특징 Span Memory
메모리 위치 | 스택 메모리에서 작동 | 힙과 스택 메모리 모두에서 작동 |
비동기 지원 | 비동기 메서드에서 사용 불가 | 비동기 메서드에서 사용 가능 |
유연성 | 동기 작업에 최적화 | 비동기 작업과 대량 데이터 처리에 적합 |
성능 | 더 빠르지만 제한적 | 약간 느리지만 더 유연함 |
【 3. 내부 작동 원리 】
3.1. Span<T>의 작동 방식
- Span<T>는 포인터와 길이 정보를 사용하여 특정 메모리 영역을 나타냅니다.
- 참조 구조체(Ref Struct)로 설계되어, 다음과 같은 제한이 있습니다:
- 힙에 저장 불가.
- 인터페이스 구현 불가.
- async 메서드에서 사용 불가.
예제: Span<T>의 메모리 구조
string text = "Hello World";
ReadOnlySpan<char> span = text.AsSpan(0, 5); // "Hello"
- span은 문자열의 첫 5문자를 참조하지만, 추가적인 메모리 할당이 없습니다.
3.2. Memory<T>의 작동 방식
- Memory<T>는 Span<T>와 유사하게 작동하지만, 힙 메모리에서 데이터를 참조할 수 있습니다.
- 비동기 작업과의 호환성을 위해 설계되었으며, 데이터를 소유자-소비자 모델로 관리합니다.
예제: Memory<T>의 메모리 구조
Memory<byte> memory = new byte[1024];
Span<byte> span = memory.Span;
- memory는 힙에서 생성된 데이터를 참조하며, span을 통해 직접 데이터를 조작할 수 있습니다.
【 4. 성능 비교 】
4.1. 테스트 조건
- 데이터: 1,000,000개의 정수를 처리.
- 환경: .NET 7, Windows 10, Intel i7, 16GB RAM.
- 비교: Span<T>, Memory<T>, 기존 배열.
4.2. 성능 테스트 코드
using System;
using System.Diagnostics;
using test;
class SpanMemoryPerformanceTest
{
static void Main()
{
// 데이터 준비
int[] data = new int[1_000_000];
for (int i = 0; i < data.Length; i++)
{
data[i] = i;
}
// Span<T> 테스트
Stopwatch stopwatch = Stopwatch.StartNew();
var span = data.AsSpan(0, 500_000);
int spanSum = 0;
foreach (var item in span)
{
spanSum += item;
}
stopwatch.Stop();
Console.WriteLine($"Span<T> 처리 시간: {stopwatch.ElapsedMilliseconds} ms, 합계: {spanSum}");
// Memory<T> 테스트
stopwatch.Restart();
var memory = new Memory<int>(data, 0, 500_000);
var memorySpan = memory.Span;
int memorySum = 0;
foreach (var item in memorySpan)
{
memorySum += item;
}
stopwatch.Stop();
Console.WriteLine($"Memory<T> 처리 시간: {stopwatch.ElapsedMilliseconds} ms, 합계: {memorySum}");
// 기존 배열 직접 접근 테스트
stopwatch.Restart();
int arraySum = 0;
for (int i = 0; i < 500_000; i++)
{
arraySum += data[i];
}
stopwatch.Stop();
Console.WriteLine($"배열 직접 접근 처리 시간: {stopwatch.ElapsedMilliseconds} ms, 합계: {arraySum}");
}
}
4.3. 테스트 결과
방법 | 처리 시간 | 합계 | 장점 | 단점 |
Span | 약 2ms | 124,999,750,000 | - 메모리 복사 없음- 빠른 데이터 슬라이싱 가능 | - 비동기 작업 불가 |
Memory | 약 3ms | 124,999,750,000 | - 비동기 작업 지원- 안전한 힙 메모리 참조 가능 | - 약간의 오버헤드- Span보다 느림 |
배열 직접 접근 | 약 1ms | 124,999,750,000 | - 가장 빠른 성능- 단순 작업에 적합 | - 복잡한 작업(슬라이싱 등)에는 부적합 |
- 빠른 속도: 배열 직접 접근 > Span > Memory
- 복잡한 작업: Span
- 비동기 작업: Memory
분석:
- Span<T>는 가장 빠르며 메모리 할당이 없습니다.
- Memory<T>는 약간 느리지만 유연성이 높아 비동기 작업에서도 사용 가능.
【 5. 실무 활용 예시 】
5.1. 문자열 조작 (Span)
string sentence = "Hello, world!";
ReadOnlySpan<char> span = sentence.AsSpan(0, 5); // "Hello"
Console.WriteLine(span.ToString());
- 문자열 복사 없이 특정 부분만 참조.
5.2. 비동기 데이터 처리 (Memory)
byte[] buffer = new byte[1024];
Memory<byte> memory = buffer.AsMemory();
await stream.ReadAsync(memory);
Process(memory.Span);
void Process(Span<byte> data)
{
// 데이터 처리
}
- 네트워크 스트림에서 데이터를 비동기적으로 읽고 처리.
5.3. 소유자-소비자 모델
using System.Buffers;
IMemoryOwner<byte> owner = MemoryPool<byte>.Shared.Rent(1024);
Memory<byte> memory = owner.Memory;
Process(memory);
owner.Dispose();
void Process(Memory<byte> memory)
{
Span<byte> span = memory.Span;
// 데이터 조작
}
- 메모리를 명시적으로 관리하며, 수동으로 해제.
【 6. 결론 】
- Span<T>는 고성능 동기 작업에 최적화되어 있으며, 메모리 복사 없이 데이터를 처리합니다.
- Memory<T>는 비동기 작업 및 대규모 데이터 처리에 적합하며, 소유자-소비자 모델을 통해 메모리를 안전하게 관리합니다.
활용 팁:
- 동기 작업 → Span<T>.
- 비동기 작업 → Memory<T>.
- 메모리 관리 최적화 → 소유자-소비자 모델 활용.
Span<T>와 Memory<T>를 활용하면 C# 애플리케이션의 성능과 안정성을 크게 향상시킬 수 있습니다.
참고 자료
https://youtu.be/Hb5QPFWm8i4?si=rfaroW17HLrRTT-E
'개발💻 > C#' 카테고리의 다른 글
익명 타입 vs record 실전 선택 가이드 (0) | 2025.04.20 |
---|---|
[C#] C#와 AI 기술 (0) | 2025.02.09 |
[C#] Dynamic 타입: 유연성과 주의점 (0) | 2024.11.30 |
[C#] 구조체와 클래스의 차이 (0) | 2024.10.06 |
[C#] readonly와 const의 차이 (0) | 2024.10.05 |