콘텐츠로 이동

C++ 객체 메모리 레이아웃과 패딩

C++ 컴파일러는 타입의 정렬 요구사항에 맞춰 구조체 멤버 사이에 패딩(padding) 바이트를 삽입합니다. 멤버 선언 순서만 바꿔도 구조체 크기가 크게 달라질 수 있으며, 이는 메모리 사용량과 캐시 성능에 직접 영향을 줍니다.


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);

// 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;
// 패딩으로 다른 스레드 데이터와 캐시 라인 분리
};

// 네트워크 패킷, 파일 포맷 등 바이트 단위 호환성이 필요할 때
#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은 정렬되지 않은 접근으로 성능 저하나 하드웨어 예외를 유발할 수 있습니다.


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 크기가 변경되었습니다. 직렬화 코드를 확인하세요.");

#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)으로 직렬화 구조체의 레이아웃이 변경되지 않도록 보호하세요.