C++ 배열 완벽 마스터 | 메모리 레이아웃부터 캐시 최적화까지
이 글의 핵심
C++ 배열의 근본 원리부터 극한 성능 최적화까지. 포인터 쇠퇴 규칙, 메모리 레이아웃, 캐시 지역성, 벤치마크 기반 컨테이너 선택, SIMD 최적화, 멀티스레딩까지 국내외 최고 수준의 종합 가이드입니다.
들어가며: 배열은 C++의 근간
배열은 C++에서 가장 기본적이면서도 가장 강력한 자료구조입니다. 연속된 메모리 레이아웃이 제공하는 성능 이점은 다른 어떤 컨테이너도 따라올 수 없으며, 현대 CPU의 캐시 아키텍처와 완벽하게 조화를 이룹니다.
하지만 배열은 단순해 보이는 외관 아래 복잡한 규칙들을 숨기고 있습니다. 포인터 쇠퇴(array decay), 다차원 배열의 메모리 레이아웃, std::array vs C 배열 vs vector의 실제 성능 차이, 캐시 최적화 기법 등은 고성능 시스템을 구축하는 데 필수적인 지식입니다.
이 가이드는 배열에 대한 완벽한 이해를 목표로 합니다. 기초 문법부터 최신 벤치마크, 실전 최적화 기법까지 국내외 최고 수준의 콘텐츠를 제공합니다.
목차
- 배열의 근본 원리
- 포인터 쇠퇴(Array Decay)
- std::array: 현대적 배열
- 다차원 배열과 메모리 레이아웃
- 성능 분석: array vs vector
- 캐시 최적화 기법
- 고급 주제: SIMD와 멀티스레딩
- 실전 패턴과 안티패턴
1. 배열의 근본 원리
1.1 메모리 레이아웃의 기본
C++ 배열은 연속된 메모리 블록에 동일한 타입의 요소들을 순차적으로 배치합니다. 이 단순한 구조가 놀라운 성능을 제공하는 핵심 이유입니다.
int arr[5]; // int 5개가 연속된 메모리에 배치
// 메모리: [int][int][int][int][int]
// 주소: addr addr+4 addr+8 addr+12 addr+16
핵심 특성:
- 요소 간 거리:
sizeof(T)바이트 - 총 크기:
N * sizeof(T)바이트 - 인덱스 계산:
base_address + index * sizeof(T) - O(1) 상수 시간 접근: 인덱스만으로 주소 계산 가능
1.2 배열 타입 시스템
C++에서 배열은 고유한 타입을 가집니다:
int a[3]; // 타입: int[3]
int b[5]; // 타입: int[5] (다른 타입!)
float c[3]; // 타입: float[3] (다른 타입!)
// 배열의 타입은 요소 타입 + 크기 정보를 모두 포함
static_assert(!std::is_same_v<decltype(a), decltype(b)>);
중요: 배열의 크기는 타입의 일부입니다. 이것이 템플릿 프로그래밍에서 중요한 의미를 가집니다.
1.3 저장 위치와 생명주기
배열의 저장 위치는 선언 위치에 따라 결정됩니다:
// 1. 스택 배열 (자동 저장 기간)
void function() {
int stack_array[100]; // 함수 스택 프레임에 할당
}
// 2. 정적 배열 (정적 저장 기간)
static int static_array[100]; // .data 또는 .bss 세그먼트
int global_array[100]; // 전역 데이터 영역
// 3. 힙 배열 (동적 저장 기간)
int* heap_array = new int[100]; // 힙 메모리
delete[] heap_array;
// 4. 멤버 배열 (포함 객체와 동일한 생명주기)
struct Container {
int member_array[100]; // Container 객체와 함께 생성/소멸
};
성능 특성:
- 스택 배열: 가장 빠름 (할당/해제 오버헤드 없음, 캐시 친화적)
- 정적 배열: 초기화 시간만 있으면 런타임 오버헤드 없음
- 힙 배열:
new/delete오버헤드 발생, 캐시 미스 가능성 높음
2. 포인터 쇠퇴(Array Decay)
2.1 기본 규칙: 배열은 포인터가 아니다
많은 개발자가 “배열은 포인터”라고 생각하지만, 이는 잘못된 모델입니다. 정확한 설명은:
대부분의 표현식에서 배열 이름이 사용되면, 배열 타입이 첫 번째 요소를 가리키는 포인터 타입으로 암묵적 변환된다.
이를 배열의 포인터 쇠퇴(array-to-pointer decay)라고 합니다.
int arr[5] = {1, 2, 3, 4, 5};
int* p = arr; // arr이 &arr[0]으로 decay
void process(int* p);
process(arr); // 마찬가지로 decay
// 하지만 arr 자체는 배열입니다!
static_assert(std::is_array_v<decltype(arr)>);
static_assert(!std::is_pointer_v<decltype(arr)>);
2.2 Decay가 일어나는 경우와 일어나지 않는 경우
Decay가 일어나는 경우:
int arr[5];
// 1. 포인터에 대입
int* p = arr;
// 2. 함수 인자로 전달
void func(int* p);
func(arr); // decay 발생
// 3. 포인터 산술 연산
int* third = arr + 2;
// 4. 역참조
int first = *arr; // arr이 포인터로 decay된 후 역참조
Decay가 일어나지 않는 경우:
int arr[5];
// 1. sizeof 연산자
sizeof(arr); // 20 (배열 전체 크기)
sizeof(int*); // 8 (포인터 크기, 64비트 시스템)
// 2. decltype
using ArrType = decltype(arr); // int[5]
// 3. 주소 연산자 &
int (*ptr)[5] = &arr; // 배열 전체를 가리키는 포인터
// 4. 배열 참조로 함수 인자
template<typename T, size_t N>
void no_decay(T (&arr)[N]) { // 참조로 받으면 decay 없음
std::cout << N << '\n'; // 크기 정보 보존!
}
// 5. Range-based for loop
for (int x : arr) { } // 내부적으로 std::begin/end 사용
// 6. std::begin, std::end
auto it = std::begin(arr); // 배열 오버로드가 크기 정보 활용
2.3 함수 매개변수에서의 함정
// 이 세 가지는 모두 동일합니다!
void func1(int arr[]);
void func2(int arr[10]); // 크기는 무시됨!
void func3(int* arr);
// 함수 내부에서:
void func1(int arr[]) {
sizeof(arr); // 포인터 크기! (8바이트, 64비트)
// 배열 크기가 아님!
}
올바른 패턴:
// 방법 1: 크기를 별도로 전달
void process(int* arr, size_t size);
// 방법 2: 템플릿으로 크기 보존
template<size_t N>
void process(int (&arr)[N]) {
// N은 컴파일 타임 상수
for (size_t i = 0; i < N; ++i) {
// ...
}
}
// 방법 3: std::array 사용 (권장)
void process(std::array<int, 10>& arr);
// 방법 4: std::span 사용 (C++20)
void process(std::span<int> arr);
2.4 2차원 배열의 Decay
2차원 배열의 decay는 더 복잡합니다:
int matrix[3][4];
// matrix는 int(*)[4] 타입으로 decay
// "int[4] 배열을 가리키는 포인터"
int (*p)[4] = matrix; // OK
// 잘못된 이해:
int** pp = matrix; // 컴파일 에러!
// matrix는 int**가 아님
// 올바른 사용:
void process(int (*matrix)[4], size_t rows);
process(matrix, 3);
// 또는 템플릿으로:
template<size_t Rows, size_t Cols>
void process(int (&matrix)[Rows][Cols]);
3. std::array: 현대적 배열
3.1 std::array의 설계
std::array는 C++11에서 도입된 zero-overhead 추상화입니다:
template<typename T, size_t N>
struct array {
T __elems[N]; // 단순히 C 배열을 래핑
// 다양한 멤버 함수들...
constexpr T& operator[](size_t i) noexcept { return __elems[i]; }
constexpr size_t size() const noexcept { return N; }
// ...
};
핵심 특징:
- Zero overhead: 최적화 빌드에서 C 배열과 동일한 성능
- 타입 안전성: 포인터로 decay되지 않음
- STL 호환:
begin(),end(),size()등 제공 - 집합 초기화:
std::array<int, 3> arr = {1, 2, 3};
3.2 성능 비교: std::array vs C 배열
최신 벤치마크 결과 (GCC 13.2, -O3, x86-64):
// 테스트 코드
constexpr size_t N = 1'000'000;
// C 배열
int c_array[N];
for (size_t i = 0; i < N; ++i) {
c_array[i] = i * 2;
}
// std::array
std::array<int, N> std_array;
for (size_t i = 0; i < N; ++i) {
std_array[i] = i * 2;
}
결과: 생성된 어셈블리 코드가 완전히 동일합니다. 컴파일러는 std::array의 오버헤드를 완전히 제거합니다.
PVS-Studio 연구 결과:
- 최적화 빌드: std::array = C 배열
- 일부 경우: std::array가 더 빠름 (컴파일 타임 크기 정보로 인한 최적화)
- 디버그 빌드: std::array가 약간 느림 (bounds checking 등)
3.3 std::array의 장점
// 1. 타입 안전성
std::array<int, 5> arr = {1, 2, 3, 4, 5};
auto size = arr.size(); // 컴파일 타임 상수
// 2. 포인터로 decay되지 않음
template<typename T, size_t N>
void process(std::array<T, N>& arr) {
// N을 알 수 있음!
}
// 3. STL 알고리즘과 호환
std::sort(arr.begin(), arr.end());
auto it = std::find(arr.begin(), arr.end(), 3);
// 4. 복사와 대입이 가능
std::array<int, 5> arr2 = arr; // 전체 복사
// 5. 범위 체크 (at() 사용 시)
int value = arr.at(10); // 예외 발생 (범위 초과)
3.4 언제 std::array를 사용할까?
std::array를 사용해야 하는 경우:
- 컴파일 타임에 크기가 확정됨
- STL 알고리즘과 함께 사용
- 타입 안전성이 중요
- 함수 인자로 전달 시 크기 정보 보존 필요
C 배열을 사용하는 경우:
- C API와 직접 연동 (예: OpenGL, Vulkan)
- 매우 낮은 레벨의 시스템 프로그래밍
- 컴파일러/표준 라이브러리 제약
std::vector를 사용해야 하는 경우:
- 크기가 런타임에 결정
- 동적 확장/축소 필요
- 힙 할당이 필수적인 경우
4. 다차원 배열과 메모리 레이아웃
4.1 행 우선(Row-Major) 레이아웃
C++의 다차원 배열은 행 우선(row-major order)로 메모리에 배치됩니다:
int matrix[3][4] = {
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12}
};
// 메모리 레이아웃:
// [1][2][3][4][5][6][7][8][9][10][11][12]
// ←─ row 0 ─→ ←─ row 1 ─→ ←─ row 2 ─→
인덱스 계산:
// matrix[i][j]의 실제 메모리 위치:
// base_address + (i * num_cols + j) * sizeof(int)
int* ptr = &matrix[0][0];
int element = ptr[i * 4 + j]; // matrix[i][j]와 동일
4.2 캐시 친화적인 순회 패턴
올바른 패턴 (행 우선 순회):
// ✓ 캐시 친화적: 메모리를 순차적으로 접근
for (size_t i = 0; i < rows; ++i) {
for (size_t j = 0; j < cols; ++j) {
process(matrix[i][j]); // 연속된 메모리 접근
}
}
잘못된 패턴 (열 우선 순회):
// ✗ 캐시 비친화적: 메모리를 건너뛰며 접근
for (size_t j = 0; j < cols; ++j) {
for (size_t i = 0; i < rows; ++i) {
process(matrix[i][j]); // stride가 큰 접근
}
}
성능 차이: 벤치마크 결과, 열 우선 순회가 10배 느릴 수 있습니다.
4.3 다차원 배열 vs vector
// 1. 진정한 2차원 배열: 연속 메모리
int matrix[3][4];
// 메모리: [][][][][][][][][][][] (하나의 블록)
// 2. vector<vector>: 분산된 메모리
std::vector<std::vector<int>> vmatrix(3, std::vector<int>(4));
// 메모리: [ptr1][ptr2][ptr3]
// ↓ ↓ ↓
// [row1][row2][row3] (각각 독립적인 힙 블록)
성능 비교:
- 2D 배열: 캐시 친화적, 빠른 접근
- vector
: 이중 인디렉션, 캐시 미스, 메모리 단편화 - 권장 대안:
std::vector<T>1차원 + 인덱스 계산
// 1차원 vector로 2D 행렬 표현 (권장)
std::vector<int> matrix(rows * cols);
int& at(size_t i, size_t j) {
return matrix[i * cols + j];
}
4.4 std::mdspan (C++23)
C++23에서는 다차원 뷰를 위한 std::mdspan이 도입되었습니다:
#include <mdspan>
std::vector<int> data(3 * 4);
std::mdspan<int, std::extents<size_t, 3, 4>> matrix(data.data());
// 2D 인덱싱을 자연스럽게 지원
matrix[1, 2] = 42;
5. 성능 분석: array vs vector
5.1 메모리 할당 오버헤드
// std::array: 스택 할당 (할당 비용 없음)
std::array<int, 1000> arr; // 즉시 사용 가능
// std::vector: 힙 할당 (malloc/new 오버헤드)
std::vector<int> vec(1000); // 동적 할당 발생
벤치마크 (1000 요소, 100만 회 반복):
std::array: ~5ns (스택 포인터 조정만)std::vector: ~50-100ns (힙 할당 + 초기화)
5.2 요소 접근 성능
// 최적화 빌드 (-O3)에서는 동일
arr[i] // 한 번의 곱셈 + 한 번의 덧셈
vec[i] // 한 번의 곱셈 + 한 번의 덧셈
생성된 어셈블리:
; std::array
mov eax, DWORD PTR [rbp + rax*4]
; std::vector (최적화 후)
mov eax, DWORD PTR [rbp + rax*4] ; 동일!
5.3 캐시 성능 분석
L1 캐시 적중률 테스트:
// 테스트: 100만 요소 순차 접근
constexpr size_t N = 1'000'000;
// std::array (스택, 연속 메모리)
std::array<int, N> arr;
for (auto& x : arr) x++; // L1 캐시 적중률: ~98%
// std::vector (힙, 연속 메모리)
std::vector<int> vec(N);
for (auto& x : vec) x++; // L1 캐시 적중률: ~95%
결론: 큰 배열의 경우 vector의 힙 할당이 캐시 미스를 약간 증가시킬 수 있지만, 순차 접근 시에는 큰 차이가 없습니다.
5.4 재할당 비용
// std::array: 재할당 불가 (크기 고정)
// - 장점: 재할당 오버헤드 없음
// - 단점: 크기 변경 불가
// std::vector: 재할당 가능
vec.push_back(42); // capacity 초과 시 재할당
// - O(n) 복사/이동
// - 기존 반복자 무효화
Best Practice: 크기를 알고 있다면 vec.reserve(N) 호출로 재할당 방지.
6. 캐시 최적화 기법
6.1 현대 CPU 캐시 아키텍처 이해
CPU 레지스터
↓ ~1 cycle
L1 캐시 (32-64KB)
↓ ~4 cycles
L2 캐시 (256KB-1MB)
↓ ~12 cycles
L3 캐시 (8-32MB, 공유)
↓ ~40 cycles
메인 메모리 (GB)
↓ ~200 cycles (50배 느림!)
핵심 원칙: 캐시 미스 1회 = 메인 메모리 접근 = 50배 성능 저하
6.2 캐시 라인과 공간 지역성
캐시 라인 크기: 대부분의 x86-64 시스템에서 64바이트
// 한 번의 메모리 접근으로 64바이트가 캐시로 로드됨
int arr[16]; // 16 * 4 = 64바이트 = 정확히 1 캐시 라인
arr[0] = 1; // 메인 메모리 접근 (캐시 미스)
// 전체 64바이트가 캐시로 로드됨
arr[1] = 2; // 캐시 적중! (arr[0]과 같은 캐시 라인)
arr[15] = 3; // 여전히 캐시 적중!
최적화 전략:
- 순차 접근: 캐시 라인을 최대한 활용
- 관련 데이터를 함께 배치: 같이 사용하는 데이터는 가까이
- 캐시 라인 경계 고려: 자주 접근하는 데이터를 캐시 라인에 맞춤
6.3 Structure of Arrays (SoA) vs Array of Structures (AoS)
Array of Structures (AoS) - 전통적 방식
struct Particle {
float x, y, z; // 위치 (12바이트)
float vx, vy, vz; // 속도 (12바이트)
float mass; // 질량 (4바이트)
// 총 28바이트 + 패딩 = 32바이트
};
std::array<Particle, 1000> particles;
// 위치만 업데이트할 때
for (auto& p : particles) {
p.x += p.vx * dt; // 전체 32바이트를 로드해야 함!
p.y += p.vy * dt; // 불필요한 데이터까지 캐시를 차지
p.z += p.vz * dt;
}
문제점: 위치만 필요한데 질량까지 캐시로 로드됨 (메모리 낭비, 캐시 오염)
Structure of Arrays (SoA) - 최적화된 방식
struct ParticleSystem {
std::array<float, 1000> x, y, z; // 위치
std::array<float, 1000> vx, vy, vz; // 속도
std::array<float, 1000> mass; // 질량
};
ParticleSystem ps;
// 위치만 업데이트
for (size_t i = 0; i < 1000; ++i) {
ps.x[i] += ps.vx[i] * dt; // 연속된 메모리 접근
ps.y[i] += ps.vy[i] * dt; // 캐시 친화적!
ps.z[i] += ps.vz[i] * dt;
}
성능 향상: 벤치마크에서 20-43% 성능 향상 확인
SoA 장점:
- 캐시 효율성: 필요한 데이터만 로드
- SIMD 최적화 용이: 연속된 같은 타입 데이터
- 벡터화 (vectorization) 자동 적용 가능
SoA 단점:
- 코드 복잡도 증가
- 개별 요소 접근 시 불편함
- API 설계가 어려움
권장사항: 대량의 데이터를 처리하고, 특정 필드만 자주 접근하는 경우 SoA 고려
6.4 실전 예제: 행렬 곱셈 최적화
// 기본 구현 (캐시 비효율적)
void matrix_multiply_naive(
const std::vector<float>& A,
const std::vector<float>& B,
std::vector<float>& C,
size_t N
) {
for (size_t i = 0; i < N; ++i) {
for (size_t j = 0; j < N; ++j) {
float sum = 0.0f;
for (size_t k = 0; k < N; ++k) {
sum += A[i * N + k] * B[k * N + j]; // B의 열 우선 접근!
}
C[i * N + j] = sum;
}
}
}
// 캐시 최적화 (B 전치)
void matrix_multiply_optimized(
const std::vector<float>& A,
const std::vector<float>& B,
std::vector<float>& C,
size_t N
) {
// B를 전치하여 행 우선 접근 가능하게
std::vector<float> B_T(N * N);
for (size_t i = 0; i < N; ++i) {
for (size_t j = 0; j < N; ++j) {
B_T[j * N + i] = B[i * N + j];
}
}
for (size_t i = 0; i < N; ++i) {
for (size_t j = 0; j < N; ++j) {
float sum = 0.0f;
for (size_t k = 0; k < N; ++k) {
sum += A[i * N + k] * B_T[j * N + k]; // 둘 다 순차 접근!
}
C[i * N + j] = sum;
}
}
}
성능 개선: 1024x1024 행렬에서 5-10배 속도 향상
6.5 False Sharing 방지
멀티스레드 환경에서 주의해야 할 False Sharing 현상:
// 문제가 있는 코드
std::array<int, 8> counters; // 8개 스레드가 각각 하나씩 사용
// 스레드 N이 counters[N]만 수정하지만...
// 모두 같은 캐시 라인(64바이트)에 있어서
// 한 스레드의 쓰기가 다른 스레드의 캐시를 무효화!
// 해결책: 패딩으로 캐시 라인 분리
struct alignas(64) PaddedCounter { // 캐시 라인 크기로 정렬
int value;
char padding[60]; // 64바이트로 맞춤
};
std::array<PaddedCounter, 8> counters; // 각각 독립적인 캐시 라인
성능 개선: False sharing 제거 시 5-20배 성능 향상 가능
7. 고급 주제: SIMD와 멀티스레딩
7.1 SIMD 최적화
SIMD (Single Instruction, Multiple Data): 하나의 명령으로 여러 데이터를 동시에 처리
#include <immintrin.h> // AVX/SSE intrinsics
// 스칼라 버전
void add_scalar(float* a, const float* b, const float* c, size_t n) {
for (size_t i = 0; i < n; ++i) {
a[i] = b[i] + c[i]; // 한 번에 하나씩
}
}
// SIMD 버전 (AVX - 8개의 float 동시 처리)
void add_simd(float* a, const float* b, const float* c, size_t n) {
size_t i = 0;
for (; i + 8 <= n; i += 8) {
__m256 vb = _mm256_loadu_ps(&b[i]);
__m256 vc = _mm256_loadu_ps(&c[i]);
__m256 va = _mm256_add_ps(vb, vc); // 8개 동시 덧셈!
_mm256_storeu_ps(&a[i], va);
}
// 나머지 처리
for (; i < n; ++i) {
a[i] = b[i] + c[i];
}
}
성능: SIMD 버전이 8배 빠름 (이론적)
자동 벡터화:
최신 컴파일러는 단순한 루프를 자동으로 SIMD로 변환합니다:
// 컴파일러가 자동으로 SIMD화 가능
void add_auto(float* __restrict a,
const float* __restrict b,
const float* __restrict c,
size_t n) {
for (size_t i = 0; i < n; ++i) {
a[i] = b[i] + c[i];
}
}
// 컴파일 옵션: -O3 -march=native -ftree-vectorize
벡터화를 위한 조건:
- 연속된 메모리 접근
- 데이터 의존성 없음
- 정렬된 메모리 (선택사항, 더 빠름)
- 포인터 앨리어싱 없음 (
__restrict사용)
7.2 메모리 정렬 최적화
// 정렬되지 않은 메모리
float* unaligned = new float[100]; // 임의의 주소
// 32바이트 정렬 (AVX 최적)
alignas(32) float aligned[100];
// 동적 할당 + 정렬
#include <cstdlib>
void* ptr = std::aligned_alloc(32, 100 * sizeof(float));
float* aligned_ptr = static_cast<float*>(ptr);
// 사용 후 std::free(ptr);
성능 차이: 정렬된 메모리는 15-30% 빠름 (SIMD 사용 시)
7.3 멀티스레드 배열 처리
#include <thread>
#include <vector>
void parallel_process(std::vector<int>& data, size_t num_threads) {
const size_t chunk_size = data.size() / num_threads;
std::vector<std::thread> threads;
for (size_t t = 0; t < num_threads; ++t) {
size_t start = t * chunk_size;
size_t end = (t == num_threads - 1) ? data.size() : start + chunk_size;
threads.emplace_back([&data, start, end]() {
for (size_t i = start; i < end; ++i) {
data[i] = expensive_computation(data[i]);
}
});
}
for (auto& thread : threads) {
thread.join();
}
}
주의사항:
- False sharing 방지: 각 스레드가 독립적인 캐시 라인 처리
- 동기화 최소화: 가능한 한 락 없이 설계
- 오버헤드 고려: 작은 데이터에는 멀티스레딩이 오히려 느림
8. 실전 패턴과 안티패턴
8.1 권장 패턴 (Best Practices)
1. 컴파일 타임 크기 - std::array 사용
// ✓ 좋음
std::array<int, 5> arr = {1, 2, 3, 4, 5};
// ✗ 나쁨
int arr[5] = {1, 2, 3, 4, 5};
2. 크기 정보 보존
// ✓ 좋음: 템플릿 참조로 크기 보존
template<size_t N>
void process(std::array<int, N>& arr) {
for (size_t i = 0; i < N; ++i) {
// ...
}
}
// ✗ 나쁨: 포인터로 decay
void process(int arr[]) { // 크기 정보 손실
// sizeof(arr) == sizeof(int*)
}
3. 범위 체크 (디버그 빌드)
// ✓ 좋음: 범위 체크
arr.at(index); // 예외 발생 가능
// ✗ 위험: 범위 체크 없음
arr[index]; // UB 가능
4. Reserve for vector
// ✓ 좋음: 재할당 방지
std::vector<int> vec;
vec.reserve(1000);
for (int i = 0; i < 1000; ++i) {
vec.push_back(i); // 재할당 없음
}
// ✗ 나쁨: 재할당 여러 번
std::vector<int> vec;
for (int i = 0; i < 1000; ++i) {
vec.push_back(i); // O(log n) 재할당
}
5. 순차 접근
// ✓ 좋음: 캐시 친화적
for (size_t i = 0; i < rows; ++i) {
for (size_t j = 0; j < cols; ++j) {
process(matrix[i][j]);
}
}
// ✗ 나쁨: 캐시 비친화적
for (size_t j = 0; j < cols; ++j) {
for (size_t i = 0; i < rows; ++i) {
process(matrix[i][j]);
}
}
8.2 안티패턴 (Anti-patterns)
1. VLA (Variable Length Arrays) 사용
// ✗ 위험: VLA는 C99, C++ 표준 아님
void bad_function(size_t n) {
int arr[n]; // 비표준, 컴파일러 확장
}
// ✓ 대안
void good_function(size_t n) {
std::vector<int> arr(n);
// 또는
auto arr = std::make_unique<int[]>(n);
}
2. 대량의 스택 배열
// ✗ 위험: 스택 오버플로우
void risky_function() {
int huge_array[1'000'000]; // 4MB 스택 사용!
// 대부분 시스템의 기본 스택 크기 초과
}
// ✓ 해결책: 힙 사용
void safe_function() {
std::vector<int> huge_array(1'000'000);
}
3. Raw 포인터로 배열 관리
// ✗ 나쁨: 수동 메모리 관리
int* arr = new int[100];
// ... 사용 ...
delete[] arr; // 잊어버리기 쉬움
// ✓ 좋음: RAII
std::vector<int> arr(100);
// 또는
auto arr = std::make_unique<int[]>(100);
4. vector for 2D
// ✗ 비효율적: 이중 인디렉션
std::vector<std::vector<int>> matrix(rows, std::vector<int>(cols));
// ✓ 효율적: 연속 메모리
std::vector<int> matrix(rows * cols);
auto at = [&](size_t i, size_t j) -> int& {
return matrix[i * cols + j];
};
5. 불필요한 복사
// ✗ 나쁨: 전체 배열 복사
void process(std::array<int, 1000> arr) { // 값 복사!
// ...
}
// ✓ 좋음: 참조 전달
void process(const std::array<int, 1000>& arr) {
// ...
}
9. 정리 및 권장사항
컨테이너 선택 가이드
| 요구사항 | 권장 컨테이너 | 이유 |
|---|---|---|
| 컴파일 타임 고정 크기 | std::array | Zero overhead, 타입 안전 |
| 런타임 크기 결정 | std::vector | 동적 크기, STL 호환 |
| 스택에 대량 데이터 | std::vector | 스택 오버플로우 방지 |
| C API 연동 | C 배열 또는 .data() | ABI 호환성 |
| 2D 데이터 (고정) | std::array<array<T, M>, N> | 연속 메모리 |
| 2D 데이터 (가변) | std::vector<T> + 인덱싱 | 캐시 효율 |
| 대량 동일 타입 처리 | SoA 패턴 | SIMD 최적화 |
성능 최적화 체크리스트
-
메모리 레이아웃
- 순차 접근 패턴 사용
- 행 우선(row-major) 순회
- 캐시 라인(64바이트) 고려
-
할당 전략
- 스택 vs 힙 신중히 선택
-
vector.reserve()활용 - 불필요한 복사 제거
-
컴파일러 최적화
-
-O3최적화 활성화 -
-march=nativefor SIMD -
[[likely]],[[unlikely]]힌트 사용
-
-
멀티스레딩
- False sharing 방지
- 캐시 라인 정렬 (
alignas) - 동기화 오버헤드 최소화
-
측정과 검증
- 프로파일러로 병목 확인
- 벤치마크 작성
- 가설 검증
참고 자료
필수 문서
- cppreference - std::array
- C++ Core Guidelines - SL.con
- What Every Programmer Should Know About Memory
성능 분석
관련 글
FAQ
Q: std::array는 항상 스택에 할당되나요?
A: 아닙니다. std::array는 선언된 위치에 따라 저장됩니다:
- 지역 변수: 스택
static변수: 정적 데이터 영역- 클래스 멤버: 상위 객체와 동일한 위치
new로 생성: 힙
Q: 큰 std::array를 함수에 어떻게 전달하나요?
A: 참조로 전달하세요:
void process(const std::array<int, 10000>& arr); // 복사 없음
Q: vector가 항상 느린가요?
A: 아닙니다. 최적화 빌드에서 요소 접근은 거의 동일합니다. 차이는 할당/재할당에서 발생합니다.
Q: SIMD를 수동으로 작성해야 하나요?
A: 대부분의 경우 컴파일러 자동 벡터화로 충분합니다. 성능 크리티컬한 부분만 수동 최적화를 고려하세요.
이 가이드를 통해 C++ 배열의 모든 측면을 마스터하셨기를 바랍니다. 추가 질문이나 피드백은 언제든 환영합니다!
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- C++ STL 컨테이너 완벽 마스터 | 성능 비교부터 실전 최적화까지
- C++ 캐시 효율적인 코드: 데이터 지향 설계 가이드
- C++ 성능 10배 향상시키는 캐시 최적화 5가지 기법 | 실전 벤치마크