언어/C#

(C# 7.2) Span<T>

tsyang 2022. 8. 7. 01:32

이게 뭐고 왜 쓰나


Provides a type-safe and memory-safe representation of a contiguous region of arbitrary memory.

 

 

마이크로소프트 Docs에서 정의하는 Span<T>의 기능이다. 사실 저게 뭔 말인가 싶지만 '배열에 대한 참조 뷰(View)를 제공하는 타입' 이라고 생각하면 된다. 그렇다 DB에서 자주 쓰이는 그 view이다.

 

그렇다면 이걸 왜 쓰는가? 힙 메모리 할당을 줄이기 위해 사용한다. 

 

Span<T> 는 readonly ref struct이다. ref struct는 오로지 스택에만 생성이 가능한 구조체이다.

 

사용 예


1. 배열에 대한 View를 제공

{
    var arr = new int[] { 0, 1, 2, 3 };

    var left = arr.Take(arr.Length/2).ToArray(); //앞에서부터 절반을 배열로
    var right = arr.Skip(arr.Length/2).ToArray(); //절반 건너뛰고 배열로

    PrintArray(left);
    PrintArray(right);
}

위와 같은 코드를 보자. {1,2,3,4} 라는 데이터가 있는 배열이 있고 이를 절반으로 나눠 출력한다. 이 과정에서 부득이하게 힙 영역에 메모리를 할당하게 된다. 심지어 left와 right에 있는 데이터는 모두 원본 배열인 arr에 있음에도 힙 영역에 메모리를 할당하고 초기화 해야 한다. 

 

Span<T>를 이용하면 힙 영역에 메모리를 생성하지 않고도 원본 배열에 대한 참조를 제공할 수 있다.

{
    var arr = new int[] { 0, 1, 2, 3 };

    // T[] => Span<T>로의 암시적 형변환
    Span<int> view = arr;

    //추가적인 힙 메모리 할당 없음
    Span<int> leftView = view.Slice(0, arr.Length/2);
    Span<int> rightView = view.Slice(arr.Length / 2);

    //원본 배열 수정됨
    rightView[0] = 99;
    
    PrintArray(arr);    //0 1 2 99 출력
    PrintArray(leftView);   //0 1 출력
    PrintArray(rightView);  //2 99 출력
}

참고로 Span<T>가 참조하는 원본 배열의 수정도 가능하다.

 

 

또한 읽기 전용으로 참조를 얻어오는 ReadOnlySpan<T> 타입도 존재한다.

{
    var arr = new int[] { 0, 1, 2, 3 };

    // T[] => Span<T>로의 암시적 형변환
    Span<int> view = arr;

    //추가적인 힙 메모리 할당 없음
    ReadOnlySpan<int> leftView = view.Slice(0, arr.Length/2);
    ReadOnlySpan<int> rightView = view.Slice(arr.Length / 2);
    
    //rightView[0] = 99;    //컴파일 에러
}

 

이런식으로 Span<T>를 활용하면 힙 메모리에 대한 할당과 초기화가 없으므로 성능이 향상되고 가비지를 만들지도 않는다.

 

2. stackalloc 사용

본디 stackalloc을 사용하려면 unsafe 구문 안에서 포인터와 함께 사용해야 했지만 Span<T>를 사용하면 그러지 않아도 된다. 

 

가령 Merge Sort를 구현하기 위해 내부적으로 임시 공간을 사용한다고 해보자.(실제로 Merge Sort는 임시 메모리 공간 없이 동작 가능하다.) 이런 임시 공간들은 가비지가 되기 때문에 성능에 좋지 않다.

{
    var leftTempArray = new int[leftArrayLength];
    var rightTempArray = new int[rightArrayLength];
}

 

그러나 stackalloc을 이용하면 스택 영역에 배열을 만들 수 있기 때문에 게임 제작에서는 이러한 방법이 꽤 활용되는데 Span<T>를 사용한다면 unsafe 구문을 사용하지 않고도 스택에 배열을 생성한 뒤 이를 다룰 수 있다.

 

{
    Span<int> leftTempArray = stackalloc int[leftArrayLength];
    Span<int> rightTempArray = stackalloc int[rightArrayLength];
}

 

다만 스택에 배열을 할당할 때는 아래와 같이 최대 사용 용량을 지정해서 사용해야 스택 오버플로를 방지할 수 있다.

const int MaxStackLimit = 1024;
Span<byte> buffer = inputLength <= MaxStackLimit ? stackalloc byte[MaxStackLimit] : new byte[inputLength];

 

 

테스트를 위해 Merge Sort 내부의 임시 배열 할당을 힙 메모리에 하도록 한다음 아래와 같은 코드를 작성했다.

private const int SIZE = 128;
private readonly static int[] arr = new int[SIZE];

static void Main(string[] args)
{
    int iter = 0;
    while (++iter < 1000)
    {
        var rand = new Random(iter);
        
        for (int i = 0; i < arr.Length; ++i)
            arr[i] = rand.Next(1, 1000000);
        
        Algorithm.MergeSortArray(arr);
        
        for (int i = 0; i < arr.Length - 1; ++i)
            if (arr[i] > arr[i + 1])
                throw new Exception("SORT ERROR");

    }
}

 

이를 실행 한 뒤 메모리를 프로파일링 해보면 ..

메모리가 점점 쌓이다가 이내 GC가 일어난다.

 

이번에는 임시 배열을 stackalloc과 Span<T>를 사용하여 스택에 할당하도록 고친 뒤 이를 실행해보면 ..

 

힙 영역에 할당하는 메모리의 크기가 훨씬 줄었음을 확인할 수 있다.

 


참고 : 정성태, 시작하세요! C# 8.0 프로그래밍 , 위키북스, 14장 C# 7.2 [780-795p]

https://docs.microsoft.com/ko-kr/dotnet/api/system.span-1?view=net-6.0 

https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/operators/stackalloc

https://stackoverflow.com/questions/14926021/buffer-overflow-protection-for-stackalloc-in-net

https://docs.microsoft.com/en-us/dotnet/standard/memory-and-spans/memory-t-usage-guidelines

'언어 > C#' 카테고리의 다른 글

Blittable Types  (1) 2023.05.13
beforefieldinit  (0) 2023.04.23
C# async/await  (0) 2022.06.12
스레드 동기화 - 기타 (이중 확인 락, 조건 변수, 컬렉션)  (1) 2022.06.05
복합 스레드 동기화 요소  (0) 2022.05.29