stackalloc 식 - 힙이 아닌 스택에 변수 저장 공간을 할당하기

stackalloc 식 - 힙이 아닌 스택에 변수 저장 공간을 할당하기

stackalloc 식은 메모리 블록을 스택에 할당해요. 보통 배열 같은 건 힙에 올라가지만, 잠깐 쓰고 버릴 작은 버퍼가 필요할 때 스택을 쓰면 훨씬 가볍거든요. 이 글에서는 stackalloc이 무엇이고, 언제 어떻게 쓰면 좋은지 하나씩 살펴볼게요.

출처: Microsoft Learn

본문

stackalloc 식은 메모리 블록을 스택에 할당해요. 메서드가 실행되는 동안 만들어진 스택 할당 메모리 블록은, 그 메서드가 반환되면 자동으로 버려져요. stackalloc으로 할당한 메모리는 직접 해제할 수 없고, 또 그럴 필요도 없어요. 스택 할당 메모리 블록은 가비지 컬렉션의 대상이 아니고, fixed으로 고정(pin)할 필요도 없어요.

stackalloc 식의 결과는 다음 유형 중 하나의 변수에 할당할 수 있어요.

  • xref:System.Span`1?displayProperty=nameWithType 또는 xref:System.ReadOnlySpan`1?displayProperty=nameWithType — 아래 예시를 볼게요.

    int length = 3;
    Span<int> numbers = stackalloc int[length];
    for (var i = 0; i < length; i++)
    {
        numbers[i] = i;
    }
    

    스택 할당 메모리 블록을 xref:System.Span`1 또는 xref:System.ReadOnlySpan`1 변수에 할당할 때는 unsafe 컨텍스트를 쓸 필요가 없어요. 포인터를 직접 다룰 때만 unsafe가 필요하니까, 안전한 유형으로는 걱정 없이 써도 되죠.

    이런 유형들을 쓸 때는 조건식이나 대입식 안에서도 stackalloc 식을 사용할 수 있어요. 아래가 그 예시예요.

    int length = 1000;
    Span<byte> buffer = length <= 1024 ? stackalloc byte[length] : new byte[length];
    

    조건에 따라 버퍼 크기가 작으면 스택에, 크면 힙에 할당하는 패턴이에요. 스택은 크기가 제한적이니, 필요한 크기에 따라 할당 위치를 고르는 방식이죠.

    xref:System.Span`1 또는 xref:System.ReadOnlySpan`1 변수가 허용되는 곳이라면 어디에서든 stackalloc 식이나 컬렉션 식을 다른 식 안에서도 자유롭게 쓸 수 있어요. 아래가 그 예시예요.

    Span<int> numbers = stackalloc[] { 1, 2, 3, 4, 5, 6 };
    var ind = numbers.IndexOfAny(stackalloc[] { 2, 4, 6, 8 });
    Console.WriteLine(ind);  // output: 1
    
    Span<int> numbers2 = [1, 2, 3, 4, 5, 6];
    var ind2 = numbers2.IndexOfAny([2, 4, 6, 8]);
    Console.WriteLine(ind2);  // output: 1
    

    메서드 인자처럼 식이 필요한 자리에 stackalloc을 바로 넣는 것도 가능해요. 첫 번째 예시에서는 IndexOfAny를 부를 때 스택 할당한 span을 그대로 넘겼고, 두 번째 예시에서는 컬렉션 식으로 같은 일을 하고 있어요.

    [!NOTE] 스택 할당 메모리를 다룰 때는 가능하면 항상 xref:System.Span`1 또는 xref:System.ReadOnlySpan`1 유형을 쓰세요.

  • 포인터 유형 — 아래가 그 예시예요.

    unsafe
    {
        int length = 3;
        int* numbers = stackalloc int[length];
        for (var i = 0; i < length; i++)
        {
            numbers[i] = i;
        }
    }
    

    포인터 유형을 다룰 때는 unsafe 컨텍스트를 반드시 써야 해요.

    [!NOTE] C# 15에서 제공되는 memory safety 미리 보기 기능을 쓰면, unsafe 컨텍스트 밖에서도 stackalloc 식을 포인터로 변환할 수 있어요. 다만 포인터를 통해 할당된 메모리에 접근하는 연산은 여전히 unsafe 컨텍스트가 필요해요.

    포인터 유형에서는 stackalloc 식을 지역 변수 선언에서 변수를 초기화할 때에만 쓸 수 있어요.

스택에서 쓸 수 있는 메모리 양은 제한적이에요. 스택에 메모리를 너무 많이 할당하면 xref:System.StackOverflowException이 발생해요. 그 예외를 피하려면 다음 규칙을 따라야 해요.

  • stackalloc으로 할당하는 메모리 양을 제한하세요. 예를 들어, 의도한 버퍼 크기가 특정 한도 아래일 때만 스택에 할당하고, 그 외에는 필요한 길이의 배열을 쓰는 거예요. 아래 코드를 볼게요.

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

    [!NOTE] 스택에서 쓸 수 있는 메모리 양은 코드가 실행되는 환경에 따라 달라지기 때문에, 실제 한도 값을 정할 때는 보수적으로 정하세요.

  • 루프 안에서 stackalloc을 쓰는 건 피하세요. 메모리 블록을 루프 밖에서 할당하고, 루프 안에서는 그 블록을 재사용하세요.

새로 할당된 메모리의 내용은 정의되어 있지 않아요. 사용하기 전에 반드시 초기화해야 해요. stackalloc 초기화 구문이나 xref:System.Span`1.Clear*?displayProperty=nameWithType 같은 메서드를 쓰면 되죠.

[!IMPORTANT] stackalloc으로 할당한 메모리를 초기화하지 않는 것은 new 연산자와의 중요한 차이점이에요. new 연산자로 할당한 메모리는 0 비트 패턴으로 초기화되지만, stackalloc은 그렇지 않아요. 그래서 초기화를 깜빡하면 쓰레기 값이 그대로 남을 수 있어요.

배열 초기화 구문을 사용해 새로 할당한 메모리의 내용을 정의할 수도 있어요. 여러 가지 방법을 아래 예시에서 보여드릴게요.

Span<int> first = stackalloc int[3] { 1, 2, 3 };
Span<int> second = stackalloc int[] { 1, 2, 3 };
ReadOnlySpan<int> third = stackalloc[] { 1, 2, 3 };

// Using collection expressions:
Span<int> fourth = [1, 2, 3];
ReadOnlySpan<int> fifth = [1, 2, 3];

stackalloc T[E] 식에서 T비관리형(unmanaged) 유형이어야 하고, E는 음수가 아닌 int 값으로 평가되어야 해요. span을 초기화할 때 컬렉션 식 구문을 쓰면, ref 안전성(ref safety)을 위반하지 않는 경우 컴파일러가 span을 위해 스택 할당 저장 공간을 쓸 수 있어요.

Security (보안)

stackalloc을 사용하면 공용 언어 런타임(CLR)에서 버퍼 오버런 감지 기능이 자동으로 켜져요. 런타임이 버퍼 오버런을 감지하면, 악성 코드가 실행될 가능성을 줄이기 위해 최대한 빨리 프로세스를 종료해요.

C# 언어 사양

자세한 내용은 C# 언어 사양스택 할당 섹션을 참고하세요.

더 알아보기