C++ 객체 메모리 레이아웃과 패딩
C++ 컴파일러는 타입의 정렬 요구사항에 맞춰 구조체 멤버 사이에 패딩(padding) 바이트를 삽입합니다. 멤버 선언 순서만 바꿔도 구조체 크기가 크게 달라질 수 있으며, 이는 메모리 사용량과 캐시 성능에 직접 영향을 줍니다.
1. 패딩 발생 원리
섹션 제목: “1. 패딩 발생 원리”struct Bad { char a; // 1바이트, offset 0 // 패딩 3바이트 (int 정렬 맞추기) int b; // 4바이트, offset 4 char c; // 1바이트, offset 8 // 패딩 3바이트 (구조체 끝 정렬)};// sizeof(Bad) == 12
struct Good { int b; // 4바이트, offset 0 char a; // 1바이트, offset 4 char c; // 1바이트, offset 5 // 패딩 2바이트};// sizeof(Good) == 8규칙: 크기가 큰 멤버를 먼저 선언하면 패딩을 최소화할 수 있습니다.
2. offsetof와 sizeof로 레이아웃 확인
섹션 제목: “2. offsetof와 sizeof로 레이아웃 확인”#include <cstddef>
struct Particle { float x, y, z; // offset 0, 4, 8 uint32_t color; // offset 12 float vx, vy, vz; // offset 16, 20, 24 float lifetime; // offset 28};// sizeof(Particle) == 32
static_assert(offsetof(Particle, color) == 12);static_assert(sizeof(Particle) == 32);3. alignas — 정렬 제어
섹션 제목: “3. alignas — 정렬 제어”// SIMD 연산을 위해 16바이트 정렬 강제struct alignas(16) SimdVector{ float x, y, z, w;};static_assert(alignof(SimdVector) == 16);
// 캐시 라인(64바이트) 정렬 — False sharing 방지struct alignas(64) ThreadLocalData{ std::atomic<int> counter; // 패딩으로 다른 스레드 데이터와 캐시 라인 분리};4. #pragma pack — 패딩 제거
섹션 제목: “4. #pragma pack — 패딩 제거”// 네트워크 패킷, 파일 포맷 등 바이트 단위 호환성이 필요할 때#pragma pack(push, 1)struct NetworkPacket{ uint8_t type; uint16_t length; uint32_t checksum; uint8_t data[256];};#pragma pack(pop)// sizeof(NetworkPacket) == 263 (패딩 없음)주의:
#pragma pack은 정렬되지 않은 접근으로 성능 저하나 하드웨어 예외를 유발할 수 있습니다.
5. Empty Base Optimization (EBO)
섹션 제목: “5. Empty Base Optimization (EBO)”struct Empty {}; // sizeof(Empty) == 1 (최소 1바이트)
struct WithMember { Empty e; // 1바이트 // 패딩 3바이트 int value; // 4바이트};// sizeof(WithMember) == 8
// EBO: 빈 기반 클래스는 크기 0으로 최적화됨struct WithBase : Empty { int value;};// sizeof(WithBase) == 4 (EBO 적용)STL 알로케이터, 정책 기반 클래스 등에서 광범위하게 활용됩니다.
6. 캐시 라인 최적화 — Hot/Cold 분리
섹션 제목: “6. 캐시 라인 최적화 — Hot/Cold 분리”// 나쁜 예: Hot 데이터와 Cold 데이터가 혼재struct Enemy { // Hot (매 프레임 접근) float x, y, z; float health; // Cold (거의 접근 안 함) std::string name; // 32바이트 std::string description; // 32바이트 int loot_table_id;};
// 좋은 예: Hot/Cold 분리struct EnemyHot { float x, y, z; float health;}; // 16바이트 — 캐시 라인 절반
struct EnemyCold { std::string name; std::string description; int loot_table_id;};
struct Enemy { EnemyHot hot; EnemyCold* cold; // 포인터로 분리};7. 구조체 크기 체크 — static_assert 활용
섹션 제목: “7. 구조체 크기 체크 — static_assert 활용”// 의도치 않은 크기 변경 방지struct Config { int32_t version; uint32_t flags; float values[8];};
static_assert(sizeof(Config) == 40, "Config 크기가 변경되었습니다. 직렬화 코드를 확인하세요.");8. std::is_standard_layout
섹션 제목: “8. std::is_standard_layout”#include <type_traits>
struct Pod { int x; float y; };struct NonPod { virtual void f() {} }; // vtable pointer 포함
static_assert(std::is_standard_layout_v<Pod>);static_assert(!std::is_standard_layout_v<NonPod>);
// Standard layout: C 구조체와 호환, memcpy 안전구조체 멤버를 크기 내림차순으로 선언하면 패딩을 최소화할 수 있습니다. 캐시 민감한 코드에서는 Hot 데이터를 한 캐시 라인(64바이트)에 집중시키고, SIMD 활용 구조체는 alignas(16/32)로 정렬하세요. static_assert(sizeof(T) == N)으로 직렬화 구조체의 레이아웃이 변경되지 않도록 보호하세요.