stackalloc

황현중·2025년 12월 11일
int* p = stackalloc int[10];
Span<int> buffer = stackalloc int[10];

처음 보면 “이게 대체 뭐지?” 싶은데, 한 번 잡고 가면
스택/힙 구조, 고성능 코드 이해에 꽤 도움이 되는 키워드다.


1. stackalloc 한 줄 정의

stackalloc은 “힙(heap)이 아니라 스택(stack)에 작은 배열(버퍼)을 직접 만들어 쓰자”는 키워드다.

보통 우리는 이렇게 배열을 만든다.

int[] arr = new int[10];
  • 이 배열은 힙(Heap)에 만들어지고
  • 사용이 끝나면 가비지 컬렉터(GC)가 언젠가 치워 준다.

반면 stackalloc을 쓰면:

int* p = stackalloc int[10]; // 스택에 int 10개짜리 버퍼 생성
  • 스택(Stack)int 10개짜리 버퍼가 만들어지고
  • 해당 메서드/블록을 빠져나가면, 스택 프레임이 정리되면서 자동으로 사라진다.

즉, 한 마디로 요약하면:

“잠깐 쓸 작은 배열을 힙 대신 스택에 올려서 빠르게 쓰고 한 번에 정리하자

2. 왜 굳이 써야 할까?

일반적인 업무 코드에서는 stackalloc을 자주 쓰지 않는다. 하지만 다음과 같은 상황에서는 꽤 유용하다.

2-1. 고성능 코드 (자주 호출되는 함수)

  • 예: 초당 수십만 번 호출되는 파싱/인코딩 함수
  • 매번 new byte[1024]를 만들어서 쓰면:
    • 힙에 계속 배열이 생기고
    • GC가 자주 돌면서 성능에 부담이 된다.
  • stackalloc으로 스택에 버퍼를 만들면:
    • 할당/해제가 엄청 빠르다 (스택 포인터만 위아래로 움직이는 수준)

2-2. 네이티브/Interop 작업

  • C, C++ DLL(P/Invoke)을 호출할 때 임시 버퍼가 필요할 수 있다.
  • 이때 byte*, int* 같은 포인터 버퍼를 stackalloc으로 만들고 바로 네이티브 함수에 넘길 수 있다.

2-3. Span<T>와 함께 쓰는 고성능 문자열/버퍼 처리

  • 예: Span<byte> buffer = stackalloc byte[256];
  • 문자열 파싱, 인코딩/디코딩, 바이너리 처리 등에서 작은 임시 버퍼를 자주 쓸 때 성능에 도움이 된다.

정리하면:

“작고, 일시적이고, 성능이 중요한 버퍼”가 필요한 경우 → stackalloc 후보

3. 기본 문법 – 두 가지 스타일

3-1. 포인터 기반 (unsafe 코드)

가장 기본 형태는 C 스타일 포인터와 함께 쓰는 것이다.

unsafe
{
    int* p = stackalloc int[5];  // 스택에 int 5개짜리 버퍼 생성

    for (int i = 0; i < 5; i++)
    {
        p[i] = i * 10;
    }

    for (int i = 0; i < 5; i++)
    {
        Console.WriteLine(p[i]);
    }
}
  • unsafe 문맥이 필요하다.
  • int*, byte* 같은 포인터 연산을 직접 쓸 수 있다.
  • C 코드와 거의 비슷한 느낌이지만, 그만큼 실수하면 위험할 수 있다.

3-2. Span<T> 기반 (요즘 추천 스타일)

C# 7.2+에서는 Span<T>와 함께 쓰면 unsafe 없이stackalloc을 사용할 수 있다.

using System;

class Program
{
    static void Main()
    {
        Span<int> buffer = stackalloc int[5]; // 스택에 int 5개

        for (int i = 0; i < buffer.Length; i++)
        {
            buffer[i] = i * 10;
        }

        foreach (var x in buffer)
        {
            Console.WriteLine(x);
        }
    }
}
  • Span<T>는 “연속된 메모리 블록”을 안전하게 다루기 위한 타입이다.
  • stackalloc으로 만든 버퍼를 Span으로 감싸면:
    • 포인터를 직접 안 써도 되고
    • 범위 체크(인덱스 검사)도 해 줘서 더 안전하다.

4. 스택에 만든다는 건 어떤 의미인가?

“스택에 만든다”는 말은 단순히 위치만 다른 게 아니라, 수명, 속도, 크기 제한에서 차이가 난다는 뜻이다.

4-1. 수명(lifetime)이 짧다

  • 스택에 올린 버퍼는 해당 메서드/블록 안에서만 유효하다.
  • 메서드에서 빠져나가는 순간 스택 프레임이 정리되면서 메모리가 같이 날아간다.
  • 그래서 바깥으로 주소/참조를 내보내면 절대 안 된다.

예를 들어, 이런 코드는 위험하다.

unsafe int* MakeBuffer()
{
    int* p = stackalloc int[10];
    return p; // ❌ 함수가 끝나면 p가 가리키는 메모리는 이미 사라진 상태
}

4-2. 할당/해제가 매우 빠르다

  • 스택은 단순히 “위로 쌓았다가, 한 번에 내려오는” 구조다.
  • 버퍼를 만들 때 스택 포인터를 조금 올리고, 함수 끝날 때 한 번에 내려온다.
  • 힙 할당 + GC 부담에 비해 엄청 가볍다.

4-3. 크기를 너무 크게 잡으면 위험하다

  • 스택은 용량이 제한적이다.
  • stackalloc byte[256] 정도는 괜찮지만, stackalloc byte[1024 * 1024] 이런 건 스택 오버플로우 위험이 있다.
  • 일반적으로 수십~수백 개 원소, 몇 KB 수준 버퍼에 사용하는 것이 적당하다.

5. new 배열과 비교 – heap vs stack

같은 int 100개라도, 다음 둘은 완전히 다르게 동작한다.

// 1) 힙에 생성
int[] arr = new int[100];

// 2) 스택에 생성
Span<int> buf = stackalloc int[100];
구분 new int[100] (Heap) stackalloc int[100] (Stack)
메모리 위치 힙(Heap) 스택(Stack)
수명 변수를 더 이상 참조하지 않을 때, GC가 수거 현재 메서드/블록 끝날 때 자동 소멸
할당 비용 상대적으로 비쌈 (힙 관리 + GC 부담) 매우 빠름 (스택 포인터 이동 수준)
크기 제한 사실상 메모리 한도까지 가능 크게 잡으면 스택 오버플로우 위험
사용 난이도 가장 일반적인 방식, 안전 잘못 쓰면 위험 → 주의 필요

6. 사용할 때 주의할 점

6-1. 버퍼 크기를 과하게 크게 잡지 말 것

  • 수백 ~ 몇 KB 정도는 보통 괜찮지만,
  • 수 MB급 버퍼를 stackalloc으로 만들면 스택 오버플로우 가능성이 크다.

6-2. 수명(scope) 벗어난 참조 금지

  • stackalloc 버퍼의 주소/참조를
    • 필드에 저장하거나
    • 리턴 값으로 내보내거나
    • 비동기/스레드로 넘기는 것
    전부 위험하다.
  • 현재 메서드/블록 안에서만 쓰고 끝내야 한다.

6-3. 진짜 필요한 곳에서만 사용

  • 일반적인 비즈니스 로직(ERP 화면, CRUD, 간단 계산)에서는 그냥 new 배열을 쓰는 것이 더 낫다.
  • stackalloc“성능/저수준 작업용 특수 도구” 정도로 생각하는 게 좋다.

7. 언제 써볼 만한지 예시

  • 초당 수십만 번 호출되는 문자열 파싱 함수에서 임시 버퍼가 필요할 때
  • 바이너리 데이터를 빠르게 처리하는 인코더/디코더 구현 시
  • P/Invoke로 C 함수에 임시 버퍼 포인터를 넘겨야 할 때
  • Span<byte>로 문자열 조작/파싱을 최적화할 때

그 외 대부분의 “일반적인” 코드에서는 stackalloc 없이도 충분하다.


8. 한 줄 요약

stackalloc = “잠깐 쓸 작은 배열을 힙이 아니라 스택에 직접 만들어서 엄청 빠르게 쓰고, 메서드 끝나면 흔적도 없이 같이 날려버리는 키워드”

0개의 댓글