C++ 메모리 정렬 | Alignment·Padding·False Sharing 완벽 정리

C++ 메모리 정렬 | Alignment·Padding·False Sharing 완벽 정리

이 글의 핵심

C++ 메모리 정렬, 패딩, alignas, alignof, False Sharing 방지, 구조체 최적화를 실전 예제와 함께 정리합니다.

들어가며

메모리 정렬(Alignment) 은 CPU가 메모리를 효율적으로 읽고 쓰기 위해 요구하는 주소 경계입니다. 컴파일러는 구조체 멤버 사이에 패딩(Padding) 을 삽입해 정렬을 맞추며, 이는 메모리 크기성능에 직접적인 영향을 미칩니다.

이 글을 읽으면

  • alignof, alignas로 정렬을 확인하고 제어합니다
  • 구조체 멤버 순서를 최적화해 메모리를 절약합니다
  • False Sharing을 방지해 멀티스레드 성능을 개선합니다
  • SIMD, 캐시 라인 최적화 등 고급 패턴을 익힙니다

목차

  1. 메모리 정렬 기본
  2. 실전 구현
  3. 고급 활용
  4. 성능 비교
  5. 실무 사례
  6. 트러블슈팅
  7. 마무리

메모리 정렬 기본

정렬이란?

CPU는 타입마다 읽기·쓰기가 허용되는 시작 주소(정렬 경계) 가 정해져 있습니다. 예를 들어, int는 4바이트 경계(주소가 4의 배수)에서 시작해야 효율적이며, 일부 CPU는 정렬되지 않은 접근을 금지하거나 성능 저하를 일으킵니다.

타입별 정렬 요구사항

#include <iostream>
using namespace std;

int main() {
    cout << "char: " << alignof(char) << endl;      // 1
    cout << "short: " << alignof(short) << endl;    // 2
    cout << "int: " << alignof(int) << endl;        // 4
    cout << "long: " << alignof(long) << endl;      // 4 (Windows) / 8 (Linux)
    cout << "double: " << alignof(double) << endl;  // 8
    cout << "int*: " << alignof(int*) << endl;      // 8 (64비트)
    
    return 0;
}

패딩이 생기는 이유

컴파일러는 각 멤버를 정렬 경계에 맞추기 위해 빈 바이트(패딩)를 삽입합니다.

struct Bad {
    char c;    // 주소 0 (1 byte)
    // 3 bytes padding (주소 1~3)
    int i;     // 주소 4 (4 bytes)
    // 4 bytes padding (주소 8~11)
    double d;  // 주소 12 (8 bytes)
};  // 총 24 bytes

실전 구현

1) 구조체 패딩 최적화

비효율적 배치

#include <iostream>
using namespace std;

struct Bad {
    char c;    // 1 byte
    // 3 bytes padding
    int i;     // 4 bytes
    // 4 bytes padding
    double d;  // 8 bytes
};  // 총 24 bytes

int main() {
    cout << "Bad: " << sizeof(Bad) << endl;  // 24
    
    return 0;
}

최적화 배치

struct Good {
    double d;  // 8 bytes
    int i;     // 4 bytes
    char c;    // 1 byte
    // 3 bytes padding
};  // 총 16 bytes

int main() {
    cout << "Good: " << sizeof(Good) << endl;  // 16
    
    return 0;
}

최적화 원칙

  1. 큰 타입을 먼저 배치 (double → int → char)
  2. 같은 크기 타입을 그룹화
  3. 패딩을 최소화
struct Best {
    double d1;  // 8 bytes
    double d2;  // 8 bytes
    int i1;     // 4 bytes
    int i2;     // 4 bytes
    char c1;    // 1 byte
    char c2;    // 1 byte
    char c3;    // 1 byte
    char c4;    // 1 byte
};  // 총 32 bytes (패딩 없음)

int main() {
    cout << "Best: " << sizeof(Best) << endl;  // 32
    
    return 0;
}

2) alignas - 정렬 지정

시그니처:

alignas(alignment) type name;

구조체 정렬

#include <iostream>
using namespace std;

struct alignas(16) Aligned {
    int x;
    int y;
};

int main() {
    cout << "정렬: " << alignof(Aligned) << endl;  // 16
    cout << "크기: " << sizeof(Aligned) << endl;   // 16
    
    return 0;
}

변수 정렬

#include <iostream>

int main() {
    alignas(64) int cacheLine[16];  // 64바이트 정렬
    
    cout << "주소: " << (uintptr_t)cacheLine << endl;
    // 64의 배수
    
    return 0;
}

3) 패딩 제거 (pragma pack)

주의: 성능 저하, undefined behavior 가능

#include <iostream>
using namespace std;

#pragma pack(push, 1)
struct Packed {
    char c;    // 1 byte
    int i;     // 4 bytes
    double d;  // 8 bytes
};  // 총 13 bytes (패딩 없음)
#pragma pack(pop)

int main() {
    cout << "Packed: " << sizeof(Packed) << endl;  // 13
    
    Packed p;
    p.i = 10;  // 정렬되지 않은 접근 (느림 또는 크래시)
    
    return 0;
}

사용 시나리오:

  • 네트워크 프로토콜 (패킷 구조)
  • 파일 포맷 (바이너리 직렬화)
  • 하드웨어 인터페이스 (레지스터 맵)

고급 활용

1) False Sharing 방지

False Sharing: 여러 스레드가 같은 캐시 라인의 다른 변수를 수정하여 성능 저하

문제 코드

#include <atomic>
#include <thread>
#include <vector>
#include <chrono>
#include <iostream>

struct Counters {
    std::atomic<int> counter1;  // 0-3 bytes
    std::atomic<int> counter2;  // 4-7 bytes
};  // 같은 캐시 라인 (64 bytes)

int main() {
    Counters counters;
    
    auto start = std::chrono::high_resolution_clock::now();
    
    std::thread t1([&]() {
        for (int i = 0; i < 10000000; ++i) {
            counters.counter1++;
        }
    });
    
    std::thread t2([&]() {
        for (int i = 0; i < 10000000; ++i) {
            counters.counter2++;
        }
    });
    
    t1.join();
    t2.join();
    
    auto end = std::chrono::high_resolution_clock::now();
    auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count();
    
    std::cout << "False Sharing: " << duration << "ms" << std::endl;
    // 약 500ms
    
    return 0;
}

해결 코드

struct CountersAligned {
    alignas(64) std::atomic<int> counter1;
    alignas(64) std::atomic<int> counter2;
};  // 각각 다른 캐시 라인

int main() {
    CountersAligned counters;
    
    auto start = std::chrono::high_resolution_clock::now();
    
    std::thread t1([&]() {
        for (int i = 0; i < 10000000; ++i) {
            counters.counter1++;
        }
    });
    
    std::thread t2([&]() {
        for (int i = 0; i < 10000000; ++i) {
            counters.counter2++;
        }
    });
    
    t1.join();
    t2.join();
    
    auto end = std::chrono::high_resolution_clock::now();
    auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count();
    
    std::cout << "No False Sharing: " << duration << "ms" << std::endl;
    // 약 150ms (3배 개선)
    
    return 0;
}

2) SIMD 정렬

SSE/AVX는 16/32바이트 정렬 필요

#include <immintrin.h>
#include <iostream>

int main() {
    // ❌ 정렬 안됨
    float data1[8];
    // __m256 a = _mm256_load_ps(data1);  // 크래시 가능
    
    // ✅ 32바이트 정렬
    alignas(32) float data2[8] = {1, 2, 3, 4, 5, 6, 7, 8};
    __m256 a = _mm256_load_ps(data2);  // 안전
    
    // 연산
    __m256 b = _mm256_set1_ps(2.0f);
    __m256 c = _mm256_mul_ps(a, b);
    
    // 결과 저장
    alignas(32) float result[8];
    _mm256_store_ps(result, c);
    
    for (float x : result) {
        std::cout << x << " ";  // 2 4 6 8 10 12 14 16
    }
    
    return 0;
}

3) 정렬된 메모리 할당

#include <cstdlib>
#include <iostream>

template<typename T, size_t Alignment = alignof(T)>
class AlignedAllocator {
public:
    using value_type = T;
    
    T* allocate(size_t n) {
        void* ptr = nullptr;
        
        #ifdef _WIN32
            ptr = _aligned_malloc(n * sizeof(T), Alignment);
        #else
            if (posix_memalign(&ptr, Alignment, n * sizeof(T)) != 0) {
                ptr = nullptr;
            }
        #endif
        
        if (!ptr) {
            throw std::bad_alloc();
        }
        
        return static_cast<T*>(ptr);
    }
    
    void deallocate(T* ptr, size_t) noexcept {
        #ifdef _WIN32
            _aligned_free(ptr);
        #else
            free(ptr);
        #endif
    }
};

int main() {
    AlignedAllocator<double, 64> allocator;
    
    double* data = allocator.allocate(100);
    
    std::cout << "주소: " << (uintptr_t)data << std::endl;
    // 64의 배수
    
    allocator.deallocate(data, 100);
    
    return 0;
}

성능 비교

정렬된 접근 vs 정렬되지 않은 접근

테스트: 1억 번 int 읽기

접근 방식시간배속
정렬된 접근 (4바이트 경계)50ms1x
정렬되지 않은 접근 (1바이트 경계)200ms0.25x

결론: 정렬된 접근이 4배 빠름

False Sharing 비교

테스트: 2개 스레드, 각 1천만 번 증가

구조시간배속
False Sharing (같은 캐시 라인)500ms1x
캐시 라인 분리 (alignas(64))150ms3.3x

결론: 캐시 라인 분리로 3배 개선

구조체 크기 비교

struct Bad {
    char c;    // 1 + 3 padding
    int i;     // 4 + 4 padding
    double d;  // 8
};  // 24 bytes

struct Good {
    double d;  // 8
    int i;     // 4
    char c;    // 1 + 3 padding
};  // 16 bytes

결론: 멤버 순서 최적화로 33% 절약


실무 사례

사례 1: 멀티스레드 카운터 - False Sharing 방지

#include <atomic>
#include <thread>
#include <vector>
#include <iostream>

struct alignas(64) AlignedCounter {
    std::atomic<int> counter;
    char padding[60];  // 64바이트 채우기
};

int main() {
    AlignedCounter counters[4];
    
    std::vector<std::thread> threads;
    for (int i = 0; i < 4; ++i) {
        threads.emplace_back([&, i]() {
            for (int j = 0; j < 1000000; ++j) {
                counters[i].counter++;
            }
        });
    }
    
    for (auto& t : threads) {
        t.join();
    }
    
    for (int i = 0; i < 4; ++i) {
        std::cout << "Counter " << i << ": " << counters[i].counter << std::endl;
    }
    
    return 0;
}

사례 2: SIMD 벡터 연산

#include <immintrin.h>
#include <iostream>

void vectorAdd(const float* a, const float* b, float* c, size_t n) {
    for (size_t i = 0; i < n; i += 8) {
        __m256 va = _mm256_load_ps(a + i);
        __m256 vb = _mm256_load_ps(b + i);
        __m256 vc = _mm256_add_ps(va, vb);
        _mm256_store_ps(c + i, vc);
    }
}

int main() {
    alignas(32) float a[8] = {1, 2, 3, 4, 5, 6, 7, 8};
    alignas(32) float b[8] = {8, 7, 6, 5, 4, 3, 2, 1};
    alignas(32) float c[8];
    
    vectorAdd(a, b, c, 8);
    
    for (float x : c) {
        std::cout << x << " ";  // 9 9 9 9 9 9 9 9
    }
    
    return 0;
}

사례 3: 네트워크 프로토콜 - 패딩 제거

#include <cstdint>
#include <iostream>

#pragma pack(push, 1)
struct PacketHeader {
    uint8_t version;     // 1 byte
    uint16_t length;     // 2 bytes
    uint32_t sequence;   // 4 bytes
    uint64_t timestamp;  // 8 bytes
};  // 총 15 bytes (패딩 없음)
#pragma pack(pop)

int main() {
    std::cout << "PacketHeader: " << sizeof(PacketHeader) << std::endl;  // 15
    
    PacketHeader header;
    header.version = 1;
    header.length = 100;
    header.sequence = 12345;
    header.timestamp = 1234567890;
    
    // 네트워크로 전송
    // send(socket, &header, sizeof(header), 0);
    
    return 0;
}

사례 4: 게임 엔진 - 데이터 지향 설계

#include <vector>
#include <iostream>

// ❌ AoS (Array of Structures) - 캐시 미스 많음
struct EntityAoS {
    float x, y, z;      // 위치
    float vx, vy, vz;   // 속도
    int health;
    int id;
};

std::vector<EntityAoS> entitiesAoS(10000);

// ✅ SoA (Structure of Arrays) - 캐시 친화적
struct EntitiesSoA {
    std::vector<float> x, y, z;
    std::vector<float> vx, vy, vz;
    std::vector<int> health;
    std::vector<int> id;
};

void updatePositions(EntitiesSoA& entities, float dt) {
    for (size_t i = 0; i < entities.x.size(); ++i) {
        entities.x[i] += entities.vx[i] * dt;
        entities.y[i] += entities.vy[i] * dt;
        entities.z[i] += entities.vz[i] * dt;
    }
}

int main() {
    EntitiesSoA entities;
    entities.x.resize(10000);
    entities.y.resize(10000);
    entities.z.resize(10000);
    entities.vx.resize(10000, 1.0f);
    entities.vy.resize(10000, 1.0f);
    entities.vz.resize(10000, 1.0f);
    
    updatePositions(entities, 0.016f);
    
    std::cout << "위치 업데이트 완료" << std::endl;
    
    return 0;
}

트러블슈팅

문제 1: 정렬되지 않은 접근

증상: 크래시 또는 성능 저하

// ❌ 정렬 안됨
char buffer[100];
int* ptr = reinterpret_cast<int*>(buffer + 1);
*ptr = 10;  // 정렬 안됨 (느림 또는 크래시)

// ✅ 정렬 보장
alignas(int) char buffer[100];
int* ptr = reinterpret_cast<int*>(buffer);
*ptr = 10;

문제 2: 구조체 크기 가정

증상: 직렬화 오류, 메모리 계산 오류

struct Data {
    char c;
    int i;
};

// ❌ 잘못된 가정
// sizeof(Data) == 5라고 가정 (실제는 8)

// ✅ sizeof 사용
size_t size = sizeof(Data);  // 8

// ✅ static_assert로 검증
static_assert(sizeof(Data) == 8, "Data size mismatch");

문제 3: 플랫폼별 차이

증상: Windows와 Linux에서 다른 크기

struct Data {
    long l;
    int i;
};

// Windows 64비트: sizeof(long) == 4
// Linux 64비트: sizeof(long) == 8

// ✅ 고정 크기 타입 사용
#include <cstdint>

struct DataFixed {
    int64_t l;  // 항상 8 bytes
    int32_t i;  // 항상 4 bytes
};

문제 4: SIMD 정렬 오류

증상: _mm256_load_ps 크래시

// ❌ 정렬 안됨
float data[8];
__m256 a = _mm256_load_ps(data);  // 크래시!

// ✅ 32바이트 정렬
alignas(32) float data[8];
__m256 a = _mm256_load_ps(data);  // 안전

// 또는 정렬되지 않은 로드 사용
__m256 a = _mm256_loadu_ps(data);  // 느리지만 안전

마무리

C++ 메모리 정렬성능메모리 효율성에 직접적인 영향을 미칩니다.

핵심 요약

  1. 정렬 기본

    • CPU는 타입별 정렬 경계 요구
    • 컴파일러는 패딩을 삽입해 정렬 맞춤
    • alignof로 확인, alignas로 제어
  2. 구조체 최적화

    • 큰 타입을 먼저 배치
    • 같은 크기 타입을 그룹화
    • 패딩을 최소화
  3. False Sharing 방지

    • 캐시 라인(64 bytes) 분리
    • alignas(64) 사용
    • 멀티스레드 성능 3배 개선
  4. SIMD 최적화

    • SSE: 16바이트 정렬
    • AVX: 32바이트 정렬
    • _mm256_load_ps vs _mm256_loadu_ps

선택 가이드

상황방법
구조체 크기 줄이기큰 타입 먼저 배치
멀티스레드 카운터alignas(64)
SIMD 연산alignas(32)
네트워크 프로토콜#pragma pack(1)

코드 예제 치트시트

// 정렬 확인
cout << alignof(int) << endl;

// 크기 확인
cout << sizeof(MyStruct) << endl;

// 정렬 지정
alignas(64) int cacheLine[16];

// 구조체 정렬
struct alignas(16) Aligned { int x, y; };

// 패딩 제거 (주의!)
#pragma pack(push, 1)
struct Packed { char c; int i; };
#pragma pack(pop)

// SIMD 정렬
alignas(32) float data[8];
__m256 a = _mm256_load_ps(data);

다음 단계

  • 캐시 최적화: C++ 캐시 최적화
  • Data-Oriented Design: C++ 캐시와 Data-Oriented Design
  • 캐시 친화적 코드: C++ 캐시 친화적 코드 작성법

참고 자료

한 줄 정리: 메모리 정렬은 성능과 직결되며, 구조체 멤버 순서 최적화와 False Sharing 방지로 멀티스레드 성능을 크게 개선할 수 있다.