← 문제로

6. 메모리 정렬, 구조체 패딩, 스택 오버플로

난이도 하
내 답안
모범답안

모범답안 — 메모리 정렬, 구조체 패딩, 스택 오버플로

난이도: 하

핵심 답변

  • 정렬(alignment): 어떤 타입의 데이터는 그 크기의 배수 주소에 놓여야(혹은 놓이는 게 효율적). 예: 8바이트 double은 8의 배수 주소에. CPU는 메모리를 워드/캐시라인 단위로 다뤄, 정렬된 접근은 한 번에 읽지만 비정렬 접근은 두 번 읽고 합쳐야 하거나(느림) 아키텍처에 따라 폴트가 난다.
  • 패딩(padding): 컴파일러가 각 멤버를 정렬 요건에 맞추려고 멤버 사이/끝에 끼워 넣는 빈 바이트. 그래서 멤버 선언 순서에 따라 구조체 크기가 달라진다. 구조체 전체 크기는 가장 큰 멤버 정렬의 배수가 되도록 끝에도 패딩(tail padding)이 붙는다.
  • 패딩 줄이기: 멤버를 크기 큰 것부터 작은 것 순으로 배치하면 중간 패딩이 최소화된다.
  • 스택 오버플로: 스택은 스레드당 크기가 작고 고정적(보통 1MB 내외). 너무 깊은 재귀나 거대한 지역 변수로 이 한도를 넘으면 가드 페이지를 건드려 크래시한다.

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

  • 왜 정렬이 빠른가: 메모리 버스/캐시는 64B 캐시라인, CPU 내부는 워드 단위로 접근한다. 8바이트 값이 8의 배수 주소에 있으면 한 캐시라인/워드 안에 깔끔히 들어가 1회 접근. 라인 경계에 걸치면(split access) 두 라인을 읽어 합쳐야 해 느리고, atomic 연산은 비정렬 시 보장이 깨질 수 있다. x86은 대부분 비정렬을 허용(성능 손해)하지만 SIMD(SSE의 movaps 등)나 일부 ARM은 비정렬 시 폴트한다.
  • 패딩 계산 예시 (Entity, 64비트)
    bool  alive   @0  (1)  + 7 패딩  → double을 8정렬 맞추려
    double posX   @8  (8)
    bool  visible @16 (1)  + 7 패딩
    double posY   @24 (8)
    int   id      @32 (4)
    bool  dirty   @36 (1)  + 3 tail 패딩 → 전체를 8의 배수로
    총 40바이트 (실제 데이터 23바이트, 패딩 17바이트)
    
    재배치(큰 것부터):
    double posX @0 (8)
    double posY @8 (8)
    int   id    @16 (4)
    bool  alive @20 (1)
    bool  visible @21 (1)
    bool  dirty @22 (1)  + 1 tail 패딩
    총 24바이트
    
    같은 데이터인데 40 → 24바이트(40% 절감).
  • 스택 메커니즘: 함수 호출마다 프레임이 쌓이고, 스택 끝엔 가드 페이지가 있어 넘어가면 페이지 폴트→크래시. 재귀 깊이 N이면 프레임 N개가 동시에 살아있어 N×(프레임 크기)가 스택 한도를 넘으면 터진다. 거대한 지역 배열(int buf[1'000'000] ≈ 4MB)도 단번에 한도 초과.

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

  • 구조체 크기와 캐시: 엔티티를 수십만 개 배열로 들고 매 틱 순회하면, 구조체가 40바이트면 24바이트일 때보다 같은 캐시라인(64B)에 더 적게 담기고 메모리 대역폭도 더 먹는다. 패딩을 줄이면 캐시 적중률·순회 속도·총 메모리가 모두 개선된다. 대량 엔티티는 패딩 절감이 곧 성능.
    • 더 나아가, 갱신 루프가 일부 필드만 쓴다면 SoA(필드별 배열) 로 바꿔 캐시라인을 100% 활용(문제 2 연계).
    • 단, false sharing이 우려되는 per-thread 핫 데이터는 일부러 캐시라인(64B)으로 패딩해 분리하기도 한다(목적이 정반대인 의도적 패딩).
  • 깊은 재귀 크래시 해결:
    • 재귀를 반복(iteration) + 명시적 스택(컨테이너) 으로 전환 → 데이터는 힙에 쌓이므로 스택 한도와 무관, 깊이도 동적으로 확장.
    • 꼬리 재귀면 컴파일러 최적화(TCO)에 기대거나 직접 루프로.
    • 정 필요하면 스레드 스택 크기를 키울 수 있으나(생성 시 지정), 근본 해법은 재귀 제거. 운영 중 입력(맵 크기)에 비례해 깊어지는 재귀는 항상 위험.

흔한 오답·함정

  • "패딩은 컴파일러 버그/낭비"라는 오해. 정렬을 맞춰 성능/정확성을 보장하는 의도된 동작이다.
  • 구조체 크기를 멤버 크기 단순 합으로 계산. 패딩을 빼먹어 틀린다(Entity는 23이 아니라 40바이트).
  • #pragma pack(1)으로 패딩을 강제로 없애면 비정렬 접근이 생겨 느려지거나(아키텍처에 따라) 폴트/원자성 깨짐. 네트워크 직렬화 등 특수 목적에만 신중히.
  • 스택 크기를 무한으로 가정. 스레드당 ~1MB는 매우 작다.
  • TLB 미스/페이지 폴트와 스택 오버플로를 혼동. 스택 오버플로는 스택 가드 페이지를 넘는 것.

꼬리질문 대비

  • Q. alignof/alignas는 무엇인가요? A. C++에서 타입/객체의 정렬 요건을 조회(alignof)하거나 지정(alignas)한다. 캐시라인 정렬(alignas(64))로 false sharing을 막거나 SIMD 정렬을 보장할 때 쓴다.
  • Q. 비정렬 접근이 항상 크래시하나요? A. 아니다. x86-64는 대부분 허용하되 느릴 수 있고, 정렬 요구 SIMD 명령이나 일부 ARM 구성에선 폴트한다. "허용되더라도 성능 손해"가 핵심.
  • Q. 큰 지역 버퍼가 필요하면? A. 힙 할당(new/vector)이나 풀에서 받는다. 또는 thread_local 재사용 버퍼. 스택엔 작고 수명이 짧은 것만.
  • Q. 스택 오버플로를 안전하게 감지할 수 있나요? A. OS가 가드 페이지로 폴트를 일으켜 보통 즉시 크래시한다(우아한 복구는 어려움). 그래서 사전에 재귀 깊이 제한/반복화로 예방하는 게 정석.