본문 바로가기
개발💻/C#

[C#] Span<T> vs Memory<T>

by Sports Entrepreneur 2025. 1. 18.

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