← 문제로

1. 스택 vs 힙, 프로세스 메모리 구조, 값/참조

난이도 하
내 답안
모범답안

모범답안 — 스택 vs 힙, 프로세스 메모리 구조, 값/참조

난이도: 하

핵심 답변

  • 프로세스 가상 주소 공간은 크게 텍스트(코드), 데이터(초기화 전역/정적), BSS(0으로 초기화되는 전역/정적), 힙(heap), 스택(stack) 으로 나뉜다. 힙은 낮은 주소에서 높은 주소로 자라고, 스택은 높은 주소에서 낮은 주소로 자라며 중간을 공유한다.
  • 스택: 함수 호출 프레임 단위로 자동 할당/해제, 포인터 증감만으로 처리되어 매우 빠름, 수명은 스코프(함수)에 묶임, 크기 제한이 작다(스레드당 보통 1MB 내외).
  • : new/malloc으로 명시적(또는 GC가 관리) 할당, 할당자가 빈 블록을 찾아야 하므로 상대적으로 느림, 수명을 프로그래머/런타임이 제어, 크기는 가상 메모리 한도까지 크다.
  • 값 타입은 변수 자체가 데이터를 직접 담고 복사 시 전체가 복제된다. 참조 타입은 변수가 힙에 있는 실제 객체를 가리키는 핸들/포인터이며 복사 시 참조(주소)만 복제된다.

깊이 있는 설명 (메커니즘, 왜)

  • 메모리 구조
    • 텍스트(.text): 기계어 코드, 보통 읽기 전용.
    • 데이터(.data): 명시적으로 초기화된 전역/정적 변수.
    • BSS(.bss): 0/미초기화 전역·정적 변수. 실행 파일에 실제 바이트를 담지 않고 크기 정보만 가져 로드시 0으로 채워진다.
    • 힙: 런타임에 동적으로 늘었다 줄었다 한다.
    • 스택: 함수 호출마다 "스택 프레임"(지역 변수, 매개변수, 반환 주소, 저장된 레지스터)이 push되고, 리턴 시 pop된다.
  • 스택이 빠른 이유: 할당은 스택 포인터(SP)를 단순히 빼는(증가시키는) 연산 한 번이다. 해제도 SP 복원 한 번. 탐색이 없다. 또한 최근에 쓴 스택 영역은 CPU 캐시에 거의 항상 살아있어 지역성이 극도로 좋다.
  • 힙이 느린 이유: 할당자가 적절한 크기의 빈 블록을 찾고(자유 리스트 탐색/병합), 메타데이터를 갱신하며, 멀티스레드 환경에선 락 또는 스레드 로컬 캐시를 다뤄야 한다. 해제 시 단편화 관리도 발생한다.
  • 값/참조 전달
    • C++: 기본적으로 값 전달은 복사 생성자 호출(또는 단순 비트 복사). 참조(T&)/포인터(T*) 전달은 주소만 넘긴다. 큰 객체는 const T&로 넘겨 복사 비용을 없앤다.
    • C#: struct는 값 타입이라 인자로 넘기면 복사, class는 참조 타입이라 참조(주소)가 복사된다. ref/in을 쓰면 값 타입도 복사 없이 넘길 수 있다.

응용/실무 연결 (게임서버에서)

  • **패턴 A (스택 지역 변수)**가 거의 항상 유리하다. Vector3 같은 작은 값 타입을 함수 안에서 만들면 스택 프레임 내부에 잡혀 추가 비용이 사실상 0이고, 함수 종료 시 자동 정리되며 GC 압박도 없다.
  • **패턴 B (new 매 틱)**는 초당 수천 번 힙 할당 → 할당자 경합, 단편화, (C#이면) GC 세대 0 압박과 주기적 GC 멈춤을 유발한다. 틱 루프 안의 불필요한 힙 할당은 게임 서버 성능 핫스팟의 단골이다.
  • "지역 변수는 무조건 스택"은 항상 맞지 않다.
    • C#에서 class 인스턴스를 지역 변수로 new하면 객체 본체는 힙에 가고 스택엔 참조만 있다. 값 타입 struct만 스택(또는 인라인)에 산다.
    • C++에서 Foo f;는 스택이지만 Foo* f = new Foo;는 힙이다. 또한 컴파일러 최적화(RVO/NRVO, escape analysis, 인라이닝)로 인해 "논리적으로 힙처럼 보이던 것"이 스택/레지스터로 바뀌거나 그 반대가 될 수 있다.

흔한 오답·함정

  • "스택은 빠르고 힙은 느리다"만 외우고 (탐색 유무, 캐시 지역성, 락)는 설명 못 하는 경우.
  • C#에서 "struct니까 스택"이라고 단정하기 — 클래스의 필드로 들어간 struct, 배열 원소인 struct, 박싱된 struct는 힙에 산다. struct가 스택에 사는 건 "지역 변수/임시값일 때"라는 조건부 사실이다.
  • 스택 크기를 무한으로 생각해 큰 배열(int buf[1000000])을 지역으로 잡다 스택 오버플로우.
  • 값 전달과 참조 전달을 "포인터냐 아니냐"로만 보고 복사 비용을 간과.

꼬리질문 대비

  • Q. 스택 오버플로우는 언제 나나요? A. 너무 깊은 재귀나 거대한 지역 배열로 스택 한도를 넘을 때. 스레드 스택은 보통 1MB 수준으로 작다.
  • Q. C#에서 struct를 언제 class 대신 쓰나요? A. 작고(보통 16바이트 이하 권장) 불변에 가까우며 값 의미가 자연스러운 경우. 단, 큰 struct는 복사 비용이 커지므로 in/ref로 넘기거나 class를 고려.
  • Q. 힙 할당을 줄이면 왜 캐시 친화적인가요? A. 스택/풀에서 연속적으로 쓰는 데이터는 메모리상 인접해 캐시라인 적중률이 높고, 산발적 힙 할당은 포인터 추적(pointer chasing)으로 캐시 미스를 늘린다.