C++ 메모리 정렬과 패딩 확인하기

네이버 블로그에 작성한 Modern CPP — OOP (2025-04-24)를 바탕으로 설명과 예제를 보완했다.

구조체의 크기는 멤버 크기의 합과 다를 수 있다. 각 타입의 정렬 요구를 맞추기 위해 멤버 사이와 객체 끝에 패딩이 들어갈 수 있기 때문이다. 정확한 배치는 컴파일러·ABI·타입에 따라 확인해야 한다.

크기와 정렬 요구를 따로 보기

sizeof(T)는 객체 크기이고 alignof(T)는 타입의 정렬 요구다. 멤버가 반드시 자신의 크기와 같은 정렬을 요구하는 것은 아니다. “8바이트 타입이면 항상 8바이트 정렬” 같은 규칙을 모든 구현에 적용하지 않는다.

다음 C++17 예제는 같은 멤버를 순서만 바꿔 배치한다.

#include <cstddef>
#include <iostream>
#include <type_traits>

struct A { char tag; double value; int count; };
struct B { double value; int count; char tag; };

int main() {
    static_assertis_standard_layout_v<A>;
    static_assertis_standard_layout_v<B>;
    std::cout << "A size=" << sizeof(A) << " align=" << alignof(A) << '\n';
    std::cout << offsetof(A, tag) << ' ' << offsetof(A, value)
              << ' ' << offsetof(A, count) << '\n';
    std::cout << "B size=" << sizeof(B) << " align=" << alignof(B) << '\n';
    std::cout << offsetof(B, value) << ' ' << offsetof(B, count)
              << ' ' << offsetof(B, tag) << '\n';
}
c++ -std=c++17 -Wall -Wextra -pedantic alignment.cpp -o alignment
./alignment

double이 8바이트 크기·정렬, int가 4바이트인 일반적인 배치에서는 A가 24바이트, B가 16바이트일 수 있다. 고정 정답으로 암기하기보다 위 프로그램의 결과와 컴파일 환경을 같이 기록한다. offsetof는 이처럼 standard-layout 타입에서 사용한다.

배열에서는 다음 원소도 정렬 조건을 만족해야 한다. 객체 끝의 패딩은 이 배치를 가능하게 하는 데 사용될 수 있다. 크기를 줄이려는 멤버 순서 변경도 공개 ABI나 직렬화 방식에 영향을 주는지 검토해야 한다. 객체 메모리를 그대로 네트워크 형식으로 보내는 것은 패딩·엔디언·버전 문제를 만든다.

정렬과 false sharing은 다른 문제다

서로 다른 스레드가 다른 변수에 쓰더라도 두 변수가 같은 캐시 라인을 공유하면 캐시 일관성 처리 때문에 성능에 영향을 줄 수 있다. 이것이 false sharing이다. 읽기만 하는 인접 데이터와는 상황이 다르며, 동일한 변수에 대한 data race와도 구분한다.

원문에 있던 alignas(32)만으로 64바이트 캐시 라인 분리가 보장되지는 않는다. 32바이트 간격의 객체 두 개가 같은 64바이트 라인에 들어갈 수 있다.

아래는 64바이트 간격을 가정한 배치 예제다. 성능 측정 예제도, 모든 CPU에서 false sharing을 없앤다는 보장도 아니다.

#include <atomic>
#include <cstddef>
#include <iostream>

struct alignas(64) Counter {
    std::atomic<unsigned long long> value{0};
};

int main() {
    static_assert(alignof(Counter) >= 64);
    static_assert(sizeof(Counter) % 64 == 0);
    Counter counters[2];
    counters[0].value.fetch_add(1, std::memory_order_relaxed);
    std::cout << "size=" << sizeof(Counter)
              << " align=" << alignof(Counter) << '\n';
    std::cout << counters[0].value.load() << '\n';
}

atomic은 해당 값에 대한 원자적 접근을 제공한다. 주변 데이터 전체를 동기화하거나 성능을 보장하지는 않는다. 실제 최적화는 스레드별 접근 패턴과 대상 CPU를 기준으로 배치 전후를 측정한다. 패딩을 늘리면 메모리 사용량과 캐시 효율이 다른 방향으로 나빠질 수도 있다.

C++17의 std::hardware_destructive_interference_size는 구현이 제공하는 간격을 참고하는 수단이다. 모든 도구 체인에서 지원되는지 확인해야 하며 실행 중인 CPU를 매번 탐지하는 함수는 아니다.

자료: C++ 정렬 요구, alignas, 하드웨어 간섭 크기