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바이트)
같은 데이터인데 40 → 24바이트(40% 절감).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바이트 - 스택 메커니즘: 함수 호출마다 프레임이 쌓이고, 스택 끝엔 가드 페이지가 있어 넘어가면 페이지 폴트→크래시. 재귀 깊이 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가 가드 페이지로 폴트를 일으켜 보통 즉시 크래시한다(우아한 복구는 어려움). 그래서 사전에 재귀 깊이 제한/반복화로 예방하는 게 정석.