C++ SIMD 최적화 실전 | SSE·AVX2·NEON 인트린직으로 4배 빠르게 [#51-2]

C++ SIMD 최적화 실전 | SSE·AVX2·NEON 인트린직으로 4배 빠르게 [#51-2]

이 글의 핵심

C++ SIMD 벡터 연산: SSE/AVX2 intrinsics, ARM NEON, 자동 벡터화, 데이터 정렬(alignment), 성능 측정. 실무 문제 시나리오와 해결법. 대량의 float 배열에 대해 반복적으로 연산하는 코드를 작성했습니다. 스칼라 루프로는 한계가 있어 보였고, SIMD(Single Instruction Multiple Data)로 바꾸니 동일 연산이 4배 빠르게 실행되었습니다.

들어가며: 같은 연산인데 4배 차이

”100만 개 float 합산이 너무 느려요”

대량의 float 배열에 대해 반복적으로 연산하는 코드를 작성했습니다. 스칼라 루프로는 한계가 있어 보였고, SIMD(Single Instruction Multiple Data)로 바꾸니 동일 연산이 4배 빠르게 실행되었습니다.

CPU는 벡터 레지스터(AVX2 기준 256비트 = float 8개)로 한 번에 여러 데이터를 처리할 수 있습니다. 한 명령으로 8개 float를 동시에 더하는 것이 가능하므로, 루프를 벡터화하면 throughput이 크게 올라갑니다. 컴파일러의 자동 벡터화가 잘 되기도 하지만, 분기·의존성·정렬 문제로 실패하는 경우가 많습니다. 이때 인트린직(intrinsics)으로 수동 벡터화를 적용하면 됩니다.

느린 코드 (스칼라):

아래 코드는 cpp를 사용한 구현 예제입니다. 반복문으로 데이터를 처리합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

// 100만 개 float 합산 - 스칼라
float sum_scalar(const float* data, size_t n) {
    float sum = 0.0f;
    for (size_t i = 0; i < n; ++i) {
        sum += data[i];
    }
    return sum;
}
// 예: 1,000,000 원소 → 약 0.8ms (예시 환경)

빠른 코드 (AVX2 SIMD):

다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#include <immintrin.h>

float sum_avx2(const float* data, size_t n) {
    __m256 vsum = _mm256_setzero_ps();
    size_t i = 0;

    for (; i + 8 <= n; i += 8) {
        __m256 v = _mm256_loadu_ps(data + i);
        vsum = _mm256_add_ps(vsum, v);
    }
    // 수평 합산
    float sum_arr[8];
    _mm256_storeu_ps(sum_arr, vsum);
    float sum = sum_arr[0] + sum_arr[1] + sum_arr[2] + sum_arr[3]
             + sum_arr[4] + sum_arr[5] + sum_arr[6] + sum_arr[7];
    for (; i < n; ++i) sum += data[i];
    return sum;
}
// 예: 1,000,000 원소 → 약 0.2ms (4배 빠름)

원인: 한 번에 8개씩 처리 vs 1개씩 처리

이 글을 읽으면:

  • SIMD의 개념과 동작 원리를 이해할 수 있습니다.
  • AVX2·NEON 인트린직으로 실전 코드를 작성할 수 있습니다.
  • 자동 벡터화 실패 시 수동 벡터화를 적용할 수 있습니다.
  • 프로덕션에서 CPU 분기·폴백 패턴을 활용할 수 있습니다.

실무 적용 경험: 이 글은 대규모 C++ 프로젝트에서 실제로 겪은 문제와 해결 과정을 바탕으로 작성되었습니다. 책이나 문서에서 다루지 않는 실전 함정과 디버깅 팁을 포함합니다.

문제 시나리오

실제 겪는 상황

아래 코드는 text를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

"100만 개 float 배열 합산이 프로파일에서 30%를 차지해요."
"이미지 픽셀 처리 루프가 너무 느려서 실시간 처리에 못 미쳐요."
"컴파일러가 벡터화했다고 하는데 실제로는 스칼라 코드가 나와요."
"AVX2를 쓰려 했는데 구형 CPU에서 Illegal instruction으로 크래시가 나요."
"_mm256_load_ps를 쓰니까 가끔 SIGSEGV가 발생해요."
"데이터가 8의 배수가 아닌데 마지막 원소가 누락돼요."

원인 후보: 자동 벡터화 실패(의존성·분기), 정렬 위반, CPU 기능 미검사, 나머지 처리 누락.

시나리오 1: 이미지 픽셀 밝기 조정

1920×1080 이미지(약 200만 픽셀)의 RGB 값을 스케일링할 때, 픽셀마다 3번 곱셈이 필요합니다. 스칼라 루프로는 수 ms가 걸리는데, SIMD로 처리하면 실시간 처리에 근접합니다.

시나리오 2: 오디오 샘플 정규화

44.1kHz 오디오 1초 분량(44,100 샘플)을 -1.0~1.0 범위로 정규화할 때, 대량의 float 연산이 필요합니다. SIMD 없이는 실시간 스트리밍에서 지연이 발생할 수 있습니다.

시나리오 3: 머신러닝 추론

CNN의 fully-connected 레이어에서 행렬-벡터 곱셈이 핵심입니다. 한 뉴런당 수천 개 가중치와 입력의 곱을 합산하는 연산이 반복되므로, SIMD로 가속하면 추론 속도가 크게 향상됩니다.

시나리오 4: 게임 물리 엔진

수백 개의 파티클 위치·속도 업데이트에서 pos += vel * dt 같은 연산이 매 프레임 반복됩니다. SIMD로 벡터화하면 프레임 드랍을 줄일 수 있습니다.

시나리오 5: 암호화/해시 연산

AES, SHA 등 암호화 알고리즘의 내부 루프는 비트 연산과 테이블 조회가 반복됩니다. AES-NI, SHA 확장 등 CPU 전용 명령이 있지만, 일반적인 비트 연산도 SIMD로 128/256비트 단위 처리하면 속도가 향상됩니다.

시나리오 6: JSON/직렬화 숫자 파싱

대량의 숫자 문자열을 float/double로 변환할 때, SIMD로 여러 자릿수를 동시에 처리하는 기법이 사용됩니다. (예: simdjson 라이브러리)

시나리오별 해결 방향

시나리오특징권장 방법
단순 배열 연산 (덧셈, 곱셈)연속 접근, 독립 반복자동 벡터화 또는 AVX2 인트린직
자동 벡터화 실패분기, 의존성수동 인트린직 벡터화
다양한 CPU 지원AVX/SSE 혼재CPU 디스패처 + 폴백
이미지/신호 처리SoA, 고정 크기 블록정렬된 버퍼 + AVX2

목차

  1. 기본 개념
  2. SSE 예제 (128비트)
  3. AVX2 완전 예제
  4. ARM NEON 예제
  5. 자동 벡터화
  6. 고급 기법 (정렬·언롤링)
  7. 자주 발생하는 에러와 해결법
  8. 베스트 프랙티스
  9. 성능 벤치마크
  10. 프로덕션 패턴
  11. 실전 예제

1. 기본 개념

SIMD란?

SIMD(Single Instruction Multiple Data)는 하나의 명령으로 여러 데이터에 동시에 연산을 수행하는 방식입니다. CPU의 벡터 레지스터(128비트 SSE, 256비트 AVX/AVX2, 512비트 AVX-512)에 여러 값을 담아 한 번에 처리합니다.

아래 코드는 mermaid를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

flowchart LR
  subgraph scalar["스칼라 (1개씩)"]
    S1[a0] --> S2[+]
    S2 --> S3[결과]
  end
  subgraph simd["SIMD (8개씩)"]
    V1[a0..a7] --> V2[한 번에 +]
    V2 --> V3[결과 8개]
  end

비유: 스칼라는 “사과를 하나씩 세는 것”, SIMD는 “사과 8개를 한 손에 들고 한 번에 세는 것”입니다.

SIMD 파이프라인 개요

다음은 mermaid를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

flowchart TB
  subgraph memory[메모리]
    M1[배열 A]
    M2[배열 B]
  end
  subgraph load[로드]
    L1[_mm256_loadu_ps]
  end
  subgraph compute[연산]
    C1[_mm256_add_ps]
  end
  subgraph store[스토어]
    S1[_mm256_storeu_ps]
  end
  M1 --> L1
  M2 --> L1
  L1 --> C1
  C1 --> S1
  S1 --> M3[결과 배열 C]

벡터 레지스터 크기

ISA레지스터float 개수double 개수
SSE128비트42
AVX/AVX2256비트84
AVX-512512비트168
ARM NEON128비트42

인트린직(Intrinsics)이란?

인트린직은 컴파일러가 제공하는 내장 함수로, SIMD 명령어에 직접 대응합니다. 예: _mm256_add_ps → 8개 float 동시 덧셈. 어셈블리보다 이식성이 좋고, 컴파일러가 레지스터 할당·스케줄링을 최적화합니다.


2. SSE 예제 (128비트)

SSE(Streaming SIMD Extensions)는 128비트 레지스터로 float 4개 또는 double 2개를 한 번에 처리합니다. 대부분의 x86-64 CPU에서 지원하며, AVX2가 없는 환경의 폴백으로 사용합니다.

SSE 헤더와 타입

아래 코드는 cpp를 사용한 구현 예제입니다. 필요한 모듈을 import하고. 코드를 직접 실행해보면서 동작을 확인해보세요.

#include <xmmintrin.h>   // SSE
#include <emmintrin.h>   // SSE2
#include <pmmintrin.h>   // SSE3
// 또는 immintrin.h로 한 번에 포함 (SSE~AVX-512)
#include <immintrin.h>

// SSE 타입: __m128 (float 4개), __m128d (double 2개), __m128i (정수)

SSE float 4개 덧셈

다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#include <emmintrin.h>

// SSE: 4개 float를 한 번에 더함 (128비트)
void add_float4_sse(const float* a, const float* b, float* c, size_t n) {
    size_t i = 0;

    for (; i + 4 <= n; i += 4) {
        __m128 va = _mm_loadu_ps(a + i);   // unaligned load
        __m128 vb = _mm_loadu_ps(b + i);
        __m128 vc = _mm_add_ps(va, vb);
        _mm_storeu_ps(c + i, vc);
    }

    for (; i < n; ++i) {
        c[i] = a[i] + b[i];
    }
}

코드 설명:

  • _mm_loadu_ps: 16바이트 정렬 불필요 (u = unaligned)
  • _mm_load_ps: 16바이트 정렬 필요 (SSE는 16바이트 경계)
  • _mm_add_ps: 4쌍 float 동시 덧셈

SSE float 4개 합산

다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#include <emmintrin.h>

float sum_sse(const float* data, size_t n) {
    __m128 vsum = _mm_setzero_ps();
    size_t i = 0;

    for (; i + 4 <= n; i += 4) {
        __m128 v = _mm_loadu_ps(data + i);
        vsum = _mm_add_ps(vsum, v);
    }

    // 수평 합산: 4개 → 1개
    __m128 shuf = _mm_movehdup_ps(vsum);   // [1,1,3,3]
    __m128 sums = _mm_add_ps(vsum, shuf);  // [0+1, 2+3]
    shuf = _mm_movehl_ps(shuf, sums);      // [2+3, 2+3]
    shuf = _mm_add_ss(sums, shuf);
    float sum = _mm_cvtss_f32(shuf);

    for (; i < n; ++i) sum += data[i];
    return sum;
}

SSE 정렬 로드 (16바이트 경계)

다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#include <emmintrin.h>

// 16바이트 정렬된 메모리에서 로드 (SSE)
void add_aligned_sse(const float* a, const float* b, float* c, size_t n) {
    size_t i = 0;
    for (; i + 4 <= n; i += 4) {
        __m128 va = _mm_load_ps(a + i);   // aligned
        __m128 vb = _mm_load_ps(b + i);
        _mm_store_ps(c + i, _mm_add_ps(va, vb));
    }
    for (; i < n; ++i) c[i] = a[i] + b[i];
}

// 정렬된 메모리 할당 (16바이트)
float* alloc_aligned_sse(size_t n) {
    return static_cast<float*>(std::aligned_alloc(16, n * sizeof(float)));
}

SSE vs AVX2: SSE는 4개 float, AVX2는 8개 float. AVX2 지원 시 _mm256_* 인트린직을 사용하면 2배 처리량이 됩니다.


3. AVX2 완전 예제

예제 1: float 배열 덧셈 (A + B → C)

다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#include <immintrin.h>
#include <cstddef>

// AVX2: 8개 float를 한 번에 더함
void add_float8_avx2(const float* a, const float* b, float* c, size_t n) {
    size_t i = 0;

    // 8개씩 벡터 처리
    for (; i + 8 <= n; i += 8) {
        __m256 va = _mm256_loadu_ps(a + i);
        __m256 vb = _mm256_loadu_ps(b + i);
        __m256 vc = _mm256_add_ps(va, vb);
        _mm256_storeu_ps(c + i, vc);
    }

    // 나머지 스칼라 처리
    for (; i < n; ++i) {
        c[i] = a[i] + b[i];
    }
}

코드 설명:

  • _mm256_loadu_ps: 정렬되지 않은 주소에서 8개 float 로드 (u = unaligned)
  • _mm256_add_ps: 8쌍 동시 덧셈
  • _mm256_storeu_ps: 결과를 메모리에 저장
  • 주의: n이 8의 배수가 아니면 나머지를 스칼라로 처리해야 함

예제 2: float 배열 합산 (수평 합산)

다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#include <immintrin.h>

float sum_avx2(const float* data, size_t n) {
    __m256 vsum = _mm256_setzero_ps();
    size_t i = 0;

    for (; i + 8 <= n; i += 8) {
        __m256 v = _mm256_loadu_ps(data + i);
        vsum = _mm256_add_ps(vsum, v);
    }

    // 수평 합산: 8개 → 1개
    __m128 lo = _mm256_castps256_ps128(vsum);
    __m128 hi = _mm256_extractf128_ps(vsum, 1);
    __m128 sum128 = _mm_add_ps(lo, hi);
    sum128 = _mm_hadd_ps(sum128, sum128);
    sum128 = _mm_hadd_ps(sum128, sum128);
    float sum = _mm_cvtss_f32(sum128);

    for (; i < n; ++i) sum += data[i];
    return sum;
}

수평 합산(horizontal add): 벡터 내 8개 값을 하나로 합치는 연산. _mm_hadd_ps를 2번 사용해 4→2→1로 줄입니다.

예제 3: 조건부 연산 (max, min)

아래 코드는 cpp를 사용한 구현 예제입니다. 필요한 모듈을 import하고, 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#include <immintrin.h>

// c[i] = max(a[i], b[i])
void max_float8_avx2(const float* a, const float* b, float* c, size_t n) {
    size_t i = 0;
    for (; i + 8 <= n; i += 8) {
        __m256 va = _mm256_loadu_ps(a + i);
        __m256 vb = _mm256_loadu_ps(b + i);
        __m256 vc = _mm256_max_ps(va, vb);
        _mm256_storeu_ps(c + i, vc);
    }
    for (; i < n; ++i) c[i] = (a[i] > b[i]) ? a[i] : b[i];
}

예제 4: FMA (Fused Multiply-Add)

c = a * b + c를 한 번에 수행. 반올림 오차가 적고 성능이 좋습니다.

아래 코드는 cpp를 사용한 구현 예제입니다. 필요한 모듈을 import하고, 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#include <immintrin.h>

// c[i] = a[i] * b[i] + c[i]
void fma_float8_avx2(const float* a, const float* b, float* c, size_t n) {
    size_t i = 0;
    for (; i + 8 <= n; i += 8) {
        __m256 va = _mm256_loadu_ps(a + i);
        __m256 vb = _mm256_loadu_ps(b + i);
        __m256 vc = _mm256_loadu_ps(c + i);
        vc = _mm256_fmadd_ps(va, vb, vc);
        _mm256_storeu_ps(c + i, vc);
    }
    for (; i < n; ++i) c[i] = a[i] * b[i] + c[i];
}

주의: _mm256_fmadd_ps는 FMA 지원 CPU에서만 동작. 컴파일 시 -mfma 필요.


4. ARM NEON 예제

ARM(Apple M1/M2, Android, 서버)에서는 NEON을 사용합니다. 128비트 레지스터로 float 4개 또는 8개(int8)를 처리합니다.

NEON float 4개 덧셈

다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#if defined(__ARM_NEON) || defined(__aarch64__)
#include <arm_neon.h>

void add_float4_neon(const float* a, const float* b, float* c, size_t n) {
    size_t i = 0;

    for (; i + 4 <= n; i += 4) {
        float32x4_t va = vld1q_f32(a + i);
        float32x4_t vb = vld1q_f32(b + i);
        float32x4_t vc = vaddq_f32(va, vb);
        vst1q_f32(c + i, vc);
    }

    for (; i < n; ++i) c[i] = a[i] + b[i];
}
#endif

NEON float 4개 합산

다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#if defined(__ARM_NEON) || defined(__aarch64__)
#include <arm_neon.h>

float sum_neon(const float* data, size_t n) {
    float32x4_t vsum = vdupq_n_f32(0.0f);
    size_t i = 0;

    for (; i + 4 <= n; i += 4) {
        float32x4_t v = vld1q_f32(data + i);
        vsum = vaddq_f32(vsum, v);
    }

    float sum = vgetq_lane_f32(vsum, 0) + vgetq_lane_f32(vsum, 1)
              + vgetq_lane_f32(vsum, 2) + vgetq_lane_f32(vsum, 3);

    for (; i < n; ++i) sum += data[i];
    return sum;
}
#endif

크로스 플랫폼 래퍼 예시

아래 코드는 cpp를 사용한 구현 예제입니다. 반복문으로 데이터를 처리합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

void add_floats(const float* a, const float* b, float* c, size_t n) {
#if defined(__AVX2__)
    add_float8_avx2(a, b, c, n);
#elif defined(__ARM_NEON) || defined(__aarch64__)
    add_float4_neon(a, b, c, n);
#else
    for (size_t i = 0; i < n; ++i) c[i] = a[i] + b[i];
#endif
}

5. 자동 벡터화

컴파일러가 루프를 분석해 SIMD로 변환하는 것을 자동 벡터화라고 합니다. 조건이 맞으면 -O3만으로도 벡터화됩니다.

자동 벡터화가 잘 되는 조건

  • 연속 메모리 접근
  • 단순 연산 (덧셈, 곱셈 등)
  • 반복 간 데이터 의존성 없음
  • 분기 최소화

자동 벡터화 실패 예시

다음은 cpp를 활용한 상세한 구현 코드입니다. 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// ❌ 의존성: 이전 반복 결과에 의존
void prefix_sum_bad(float* data, size_t n) {
    for (size_t i = 1; i < n; ++i) {
        data[i] += data[i - 1];  // data[i-1]이 직전에 변경됨
    }
}

// ❌ 간접 접근: 인덱스가 연속이 아님
void gather_bad(const float* src, const int* indices, float* dst, size_t n) {
    for (size_t i = 0; i < n; ++i) {
        dst[i] = src[indices[i]];
    }
}

// ❌ 복잡한 분기
void branch_heavy(float* data, size_t n) {
    for (size_t i = 0; i < n; ++i) {
        if (data[i] > 0) data[i] *= 2;
        else data[i] /= 2;
    }
}

벡터화 힌트 (GCC/Clang)

아래 코드는 cpp를 사용한 구현 예제입니다. 반복문으로 데이터를 처리합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

// #pragma GCC ivdep: 반복 간 의존성 없음을 힌트
#pragma GCC ivdep
for (size_t i = 0; i < n; ++i) {
    c[i] = a[i] + b[i];
}

벡터화 리포트 확인

컴파일러가 어떤 루프를 벡터화했는지 확인할 수 있습니다.

아래 코드는 bash를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

# GCC: 벡터화 리포트
g++ -O3 -march=native -fopt-info-vec-optimized simd_test.cpp -o simd_test

# Clang: 벡터화 상세
clang++ -O3 -march=native -Rpass=loop-vectorize -Rpass-missed=loop-vectorize simd_test.cpp -o simd_test

출력 예시: simd_test.cpp:15:5: remark: vectorized loop (vectorization width 8, interleaved count 2)

자동 벡터화 성공 예시

다음은 간단한 cpp 코드 예제입니다. 반복문으로 데이터를 처리합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

// ✅ 연속 접근, 독립 반복 → 자동 벡터화 잘 됨. restrict로 별칭 없음 힌트 가능
void add_auto(const float* a, const float* b, float* c, size_t n) {
    for (size_t i = 0; i < n; ++i) c[i] = a[i] + b[i];
}

컴파일 옵션

아래 코드는 bash를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

# AVX2 타겟 (x86-64)
g++ -O3 -march=native -o simd_test simd_test.cpp

# FMA 포함 (dot product 등에 유리)
g++ -O3 -march=native -mfma -o simd_test simd_test.cpp

# NEON (ARM)
clang++ -O3 -arch arm64 -o simd_test simd_test.cpp

-march=native: 현재 CPU가 지원하는 최대 ISA까지 사용 (개발용). 배포 시에는 -march=haswell 등 구체적 타겟 지정을 권장합니다.


6. 고급 기법

데이터 정렬 (Aligned Load/Store)

정렬된 주소에서 로드/스토리하면 일부 CPU에서 더 빠를 수 있습니다.

ISA정렬 요구바이트 경계
SSE16바이트alignas(16)
AVX/AVX232바이트alignas(32)
AVX-51264바이트alignas(64)

다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#include <immintrin.h>
#include <cstdlib>

void add_aligned(const float* a, const float* b, float* c, size_t n) {
    // 32바이트 정렬 가정 (AVX2)
    size_t i = 0;
    for (; i + 8 <= n; i += 8) {
        __m256 va = _mm256_load_ps(a + i);   // aligned
        __m256 vb = _mm256_load_ps(b + i);
        _mm256_store_ps(c + i, _mm256_add_ps(va, vb));
    }
    for (; i < n; ++i) c[i] = a[i] + b[i];
}

// 정렬된 메모리 할당 (C++17)
float* alloc_aligned(size_t n) {
    return static_cast<float*>(std::aligned_alloc(32, n * sizeof(float)));
}

// C++11: posix_memalign
float* alloc_aligned_posix(size_t n) {
    void* p = nullptr;
    if (posix_memalign(&p, 32, n * sizeof(float)) != 0)
        return nullptr;
    return static_cast<float*>(p);
}

정렬 검증: 디버그 빌드에서 assert(reinterpret_cast<uintptr_t>(ptr) % 32 == 0)로 검증 가능.

주의: _mm256_load_ps에 정렬되지 않은 주소를 넘기면 SIGSEGV 또는 잘못된 결과가 발생할 수 있습니다.

루프 언롤링

한 번에 처리하는 원소 수를 늘려 분기 비용을 줄입니다.

다음은 cpp를 활용한 상세한 구현 코드입니다. 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

void add_unrolled(const float* a, const float* b, float* c, size_t n) {
    size_t i = 0;
    for (; i + 32 <= n; i += 32) {
        __m256 va0 = _mm256_loadu_ps(a + i + 0);
        __m256 vb0 = _mm256_loadu_ps(b + i + 0);
        _mm256_storeu_ps(c + i + 0, _mm256_add_ps(va0, vb0));

        __m256 va1 = _mm256_loadu_ps(a + i + 8);
        __m256 vb1 = _mm256_loadu_ps(b + i + 8);
        _mm256_storeu_ps(c + i + 8, _mm256_add_ps(va1, vb1));

        __m256 va2 = _mm256_loadu_ps(a + i + 16);
        __m256 vb2 = _mm256_loadu_ps(b + i + 16);
        _mm256_storeu_ps(c + i + 16, _mm256_add_ps(va2, vb2));

        __m256 va3 = _mm256_loadu_ps(a + i + 24);
        __m256 vb3 = _mm256_loadu_ps(b + i + 24);
        _mm256_storeu_ps(c + i + 24, _mm256_add_ps(va3, vb3));
    }
    for (; i < n; ++i) c[i] = a[i] + b[i];
}

Gather / Scatter (AVX2)

Gather: 서로 다른 주소에서 값을 모아 벡터로 로드
Scatter: 벡터 값을 서로 다른 주소에 저장

간접 접근이 필요할 때 사용합니다. AVX2에서는 _mm256_i32gather_ps 등이 있습니다.

아래 코드는 cpp를 사용한 구현 예제입니다. 필요한 모듈을 import하고. 코드를 직접 실행해보면서 동작을 확인해보세요.

#include <immintrin.h>

// indices[]가 가리키는 위치에서 8개 float 수집
void gather_example(const float* base, const int* indices, float* result) {
    __m256i vidx = _mm256_loadu_si256(reinterpret_cast<const __m256i*>(indices));
    __m256 v = _mm256_i32gather_ps(base, vidx, 4);  // stride 4 = sizeof(float)
    _mm256_storeu_ps(result, v);
}

주의: Gather/Scatter는 연속 로드보다 느릴 수 있어, 가능하면 데이터 레이아웃을 SoA로 바꾸는 것이 좋습니다.

SoA (Structure of Arrays)

AoS(Array of Structures)보다 SoA가 벡터화에 유리합니다.

다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// ❌ AoS: 연속된 필드가 아님
struct Particle { float x, y, z, vx, vy, vz; };
Particle particles[1000];

// ✅ SoA: 같은 타입이 연속
struct Particles {
    float x[1000], y[1000], z[1000];
    float vx[1000], vy[1000], vz[1000];
};

void update_soa(Particles& p, float dt, size_t n) {
    for (size_t i = 0; i + 8 <= n; i += 8) {
        __m256 vx = _mm256_loadu_ps(p.vx + i);
        __m256 dx = _mm256_mul_ps(vx, _mm256_set1_ps(dt));
        __m256 x = _mm256_loadu_ps(p.x + i);
        _mm256_storeu_ps(p.x + i, _mm256_add_ps(x, dx));
        // y, z도 동일
    }
}

7. 자주 발생하는 에러와 해결법

문제 1: 정렬되지 않은 메모리로 _mm256_load_ps 사용

증상: SIGSEGV 또는 잘못된 결과

원인: _mm256_load_ps는 32바이트 정렬을 요구합니다.

해결법:

아래 코드는 cpp를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// ❌ 잘못된 사용
float* data = new float[n];
__m256 v = _mm256_load_ps(data);  // data가 정렬되지 않을 수 있음

// ✅ 올바른 사용: loadu 사용
__m256 v = _mm256_loadu_ps(data);

// 또는 정렬된 메모리 할당
float* data = static_cast<float*>(std::aligned_alloc(32, n * sizeof(float)));
__m256 v = _mm256_load_ps(data);

문제 2: 배열 크기가 벡터 크기의 배수가 아님

증상: 마지막 원소 누락 또는 버퍼 오버런

해결법:

// ✅ 나머지 스칼라 처리
for (; i + 8 <= n; i += 8) { /* 벡터 처리 */ }
for (; i < n; ++i) { /* 스칼라 처리 */ }

문제 3: CPU 기능 미지원에서 AVX2 실행

증상: Illegal instruction (SIGILL)

원인: 구형 CPU에서 AVX2 미지원

해결법: 런타임 CPU 기능 검사 후 폴백 (아래 프로덕션 패턴 참조)

문제 4: 수평 합산 순서 오류

증상: 합산 결과가 틀림

아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

// ❌ 잘못된 수평 합산
float sum = sum_arr[0];  // 8개 중 1개만 사용

// ✅ 올바른 수평 합산
float sum = sum_arr[0] + sum_arr[1] + sum_arr[2] + sum_arr[3]
          + sum_arr[4] + sum_arr[5] + sum_arr[6] + sum_arr[7];

문제 5: 컴파일 옵션 누락

증상: 인트린직이 스칼라 코드로 컴파일됨

해결법:

아래 코드는 bash를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

# AVX2 활성화
g++ -O3 -mavx2 -mfma -o app app.cpp

# 또는
g++ -O3 -march=native -o app app.cpp

문제 6: 캐시 라인 분할 (False Sharing)

증상: 멀티스레드 + SIMD에서 예상보다 느림

원인: 여러 스레드가 인접한 메모리에 쓰면, 서로 다른 캐시 라인이 같은 캐시 라인을 공유해 쓰기 충돌이 발생합니다.

해결법: 스레드별 데이터를 캐시 라인(64바이트) 경계로 정렬하고 패딩을 둡니다.

다음은 간단한 cpp 코드 예제입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 코드를 직접 실행해보면서 동작을 확인해보세요.

struct alignas(64) ThreadData {
    float sum;
    char padding[64 - sizeof(float)];
};

문제 7: int와 float 혼용

증상: float 벡터에 int 인트린직 사용 시 컴파일 에러

해결법: 타입에 맞는 인트린직 사용. _mm256_add_ps(float) vs _mm256_add_epi32(int32).

아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

// float
__m256 va = _mm256_loadu_ps(a);
__m256 vb = _mm256_add_ps(va, vb);

// int32
__m256i va = _mm256_loadu_si256(reinterpret_cast<const __m256i*>(a));
__m256i vb = _mm256_add_epi32(va, vb);

문제 8: 부동소수점 연산 순서 차이

증상: 스칼라와 SIMD 결과가 소수점 아래에서 미세하게 다름. 원인: 병렬 처리로 연산 순서 변경. 해결법: 대부분 허용 범위. 수치 안정성 중요 시 Kahan 합산 등 고려.

문제 9: 벡터화 힌트 오용

증상: #pragma GCC ivdep 사용해도 벡터화 안 됨. 원인: 힌트일 뿐, 실제 의존성 있으면 무시됨. 해결법: 의존성 제거 또는 수동 인트린직.

문제 10: AVX-512 클럙 다운

증상: AVX-512 사용 시 CPU 클럭 낮아져 오히려 느려짐. 원인: 전력 소비 증가. 해결법: 벤치마크로 실제 이득 확인. 작은 데이터는 AVX2가 유리할 수 있음.


8. 베스트 프랙티스

1. 프로파일링 우선

SIMD 최적화 전에 반드시 프로파일링으로 병목을 확인합니다. 병목이 아닌 루프에 SIMD를 적용하면 유지보수만 복잡해집니다.

✅ 순서: 프로파일 → 병목 확인 → 자동 벡터화 시도 → 수동 인트린직
❌ 순서: "일단 SIMD로 바꿔보자"

2. 자동 벡터화 먼저 시도

-O3 -march=native로 컴파일하고, 루프를 단순하게 유지합니다. 컴파일러가 벡터화할 수 있으면 수동 인트린직보다 유지보수가 쉽습니다.

아래 코드는 cpp를 사용한 구현 예제입니다. 반복문으로 데이터를 처리합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

// ✅ 자동 벡터화 유리: 연속 접근, 단순 연산
for (size_t i = 0; i < n; ++i) {
    c[i] = a[i] + b[i];
}

// ❌ 자동 벡터화 불리: 의존성
for (size_t i = 1; i < n; ++i) {
    data[i] += data[i - 1];
}

3. 정렬 규칙 일관 적용

  • 불확실하면 loadu/storeu 사용: _mm256_loadu_ps, _mm256_storeu_ps
  • 정렬이 보장되면 load/store 사용: std::aligned_alloc, alignas 활용
  • 혼용 금지: 같은 함수 내에서 정렬 가정을 섞지 않음

4. 나머지 처리 필수

배열 크기가 벡터 크기(8 for AVX2 float)의 배수가 아니면 반드시 스칼라 루프로 나머지를 처리합니다.

// ✅ 올바른 패턴
for (; i + 8 <= n; i += 8) { /* 벡터 */ }
for (; i < n; ++i) { /* 스칼라 */ }

5. CPU 기능 런타임 검사

배포 바이너리는 AVX2 미지원 CPU에서도 동작해야 합니다. 런타임에 CPU 기능을 검사하고 폴백 경로를 제공합니다.

6. SoA 데이터 레이아웃

파티클·행렬 등 반복 처리할 데이터는 SoA(Structure of Arrays)로 배치하면 벡터화가 쉽습니다.

7. 벤치마크로 검증

SIMD 적용 후 실제 환경에서 벤치마크를 수행합니다. 데이터 크기·캐시 상태에 따라 스칼라가 더 빠른 경우도 있습니다.


9. 성능 벤치마크

테스트 환경 (예시)

  • CPU: Intel Core i7-12700 (AVX2 지원)
  • 컴파일: g++ -O3 -march=native
  • 데이터: 1,000,000 float

벤치마크 결과 (예시)

연산스칼라SSE (4개)AVX2 (8개)배속
배열 덧셈 (A+B→C)0.45 ms0.18 ms0.12 ms3.8x
배열 합산0.82 ms0.35 ms0.21 ms3.9x
max(A,B)→C0.48 ms0.20 ms0.13 ms3.7x
FMA (A*B+C)0.95 ms0.42 ms0.25 ms3.8x

참고: SSE는 AVX2의 약 절반 처리량, 스칼라 대비 약 2배 개선.

벤치마크 코드 예시

다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#include <chrono>
#include <iostream>
#include <vector>

template <typename Func>
double benchmark(Func&& f, int iterations = 100) {
    auto start = std::chrono::high_resolution_clock::now();
    for (int i = 0; i < iterations; ++i) f();
    auto end = std::chrono::high_resolution_clock::now();
    return std::chrono::duration<double, std::milli>(end - start).count() / iterations;
}

int main() {
    const size_t n = 1000000;
    std::vector<float> a(n, 1.0f), b(n, 2.0f), c(n);

    double t_scalar = benchmark([&]() {
        for (size_t i = 0; i < n; ++i) c[i] = a[i] + b[i];
    });
    double t_simd = benchmark([&]() {
        add_float8_avx2(a.data(), b.data(), c.data(), n);
    });

    std::cout << "Scalar: " << t_scalar << " ms\n";
    std::cout << "SIMD:   " << t_simd << " ms\n";
    std::cout << "Speedup: " << (t_scalar / t_simd) << "x\n";
    return 0;
}

10. 프로덕션 패턴

패턴 1: CPU 디스패처 (런타임 분기)

배포 바이너리가 여러 CPU에서 실행될 때, 런타임에 CPU 기능을 검사해 적절한 구현을 선택합니다.

다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#include <cpuid.h>
#include <cstring>

bool has_avx2() {
    unsigned int eax, ebx, ecx, edx;
    __cpuid_count(7, 0, eax, ebx, ecx, edx);
    return (ebx & (1 << 5)) != 0;  // EBX bit 5: AVX2
}

using AddFunc = void (*)(const float*, const float*, float*, size_t);

void add_dispatcher(const float* a, const float* b, float* c, size_t n) {
    static AddFunc fn =  -> AddFunc {
        if (has_avx2()) return add_float8_avx2;
        return add_scalar;
    }();
    fn(a, b, c, n);
}

void add_scalar(const float* a, const float* b, float* c, size_t n) {
    for (size_t i = 0; i < n; ++i) c[i] = a[i] + b[i];
}

주의: __cpuid는 x86 전용. ARM에서는 getauxval 등으로 NEON 지원 여부를 확인합니다.

패턴 2: 컴파일 타임 분기

아래 코드는 cpp를 사용한 구현 예제입니다. 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

template <bool UseSIMD>
void add_impl(const float* a, const float* b, float* c, size_t n) {
    if constexpr (UseSIMD) {
        add_float8_avx2(a, b, c, n);
    } else {
        for (size_t i = 0; i < n; ++i) c[i] = a[i] + b[i];
    }
}

패턴 3: SIMD 라이브러리 활용

플랫폼 차이를 추상화하려면 xsimd, Highway, Vc 같은 라이브러리를 사용할 수 있습니다.

다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// xsimd 예시 (의사 코드)
#include <xsimd/xsimd.hpp>
namespace xs = xsimd;

void add_xsimd(const float* a, const float* b, float* c, size_t n) {
    using batch = xs::batch<float>;
    size_t simd_size = batch::size;
    size_t i = 0;
    for (; i + simd_size <= n; i += simd_size) {
        auto va = batch::load_unaligned(a + i);
        auto vb = batch::load_unaligned(b + i);
        (va + vb).store_unaligned(c + i);
    }
    for (; i < n; ++i) c[i] = a[i] + b[i];
}

패턴 4: std::execution과 병행

SIMD로 벡터화하고, std::execution::par로 멀티코어를 활용할 수 있습니다.

아래 코드는 cpp를 사용한 구현 예제입니다. 필요한 모듈을 import하고. 코드를 직접 실행해보면서 동작을 확인해보세요.

#include <algorithm>
#include <execution>

std::transform(std::execution::par, a.begin(), a.end(), b.begin(), c.begin(),
                { return x + y; });

패턴 5: 빌드 타겟 분리

여러 ISA 지원 시 컴파일 유닛을 나눕니다. simd_avx2.cpp-mavx2로, simd_scalar.cpp는 기본 옵션으로 컴파일 후 런타임에 함수 포인터로 선택합니다.

패턴 6: SIMD를 쓰지 말아야 할 때

  • 데이터 양이 적을 때 (수백 개 미만): 벡터화 오버헤드가 이득을 상쇄할 수 있음
  • 분기가 많은 로직: 벡터화해도 마스킹 비용이 커서 이득이 적음
  • 이식성이 최우선일 때: 순수 C++ 표준만 쓰는 것이 유지보수에 유리
  • 프로파일링 전: 병목이 SIMD로 해결되는 영역인지 먼저 확인

11. 실전 예제

예제: 이미지 밝기 조정 (RGBA)

다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#include <immintrin.h>
#include <cstddef>

// RGBA 픽셀: 4바이트씩. 8픽셀 = 32바이트
void brighten_avx2(uint8_t* pixels, size_t num_pixels, float factor) {
    size_t i = 0;
    const __m256 vfactor = _mm256_set1_ps(factor);

    for (; i + 32 <= num_pixels * 4; i += 32) {
        // 32바이트 = 8개 float (RGBA 8픽셀)
        __m256 v = _mm256_cvtepi32_ps(
            _mm256_cvtepu8_epi32(_mm_loadl_epi64(
                reinterpret_cast<const __m128i*>(pixels + i))));
        v = _mm256_mul_ps(v, vfactor);
        v = _mm256_min_ps(v, _mm256_set1_ps(255.0f));
        __m256i vi = _mm256_cvtps_epi32(v);
        // 패킹 후 저장 (실제로는 더 복잡)
    }
    // 나머지 스칼라 처리
}

참고: 실제 RGBA는 8→32비트 변환, 클램핑, 패킹이 필요해 코드가 더 길어집니다.

예제: Dot Product (내적)

다음은 cpp를 활용한 상세한 구현 코드입니다. 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

float dot_avx2(const float* a, const float* b, size_t n) {
    __m256 vsum = _mm256_setzero_ps();
    size_t i = 0;

    for (; i + 8 <= n; i += 8) {
        __m256 va = _mm256_loadu_ps(a + i);
        __m256 vb = _mm256_loadu_ps(b + i);
        vsum = _mm256_fmadd_ps(va, vb, vsum);
    }

    float sum_arr[8];
    _mm256_storeu_ps(sum_arr, vsum);
    float sum = sum_arr[0] + sum_arr[1] + sum_arr[2] + sum_arr[3]
             + sum_arr[4] + sum_arr[5] + sum_arr[6] + sum_arr[7];

    for (; i < n; ++i) sum += a[i] * b[i];
    return sum;
}

예제: ReLU 활성화 함수

딥러닝에서 자주 쓰이는 ReLU: y = max(0, x)

아래 코드는 cpp를 사용한 구현 예제입니다. 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

void relu_avx2(const float* x, float* y, size_t n) {
    __m256 zero = _mm256_setzero_ps();
    size_t i = 0;

    for (; i + 8 <= n; i += 8) {
        __m256 vx = _mm256_loadu_ps(x + i);
        __m256 vy = _mm256_max_ps(zero, vx);
        _mm256_storeu_ps(y + i, vy);
    }
    for (; i < n; ++i) y[i] = (x[i] > 0) ? x[i] : 0.0f;
}

예제: 행렬-벡터 곱셈 (일부)

다음은 cpp를 활용한 상세한 구현 코드입니다. 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// y = A * x (A: rows x cols)
void matvec_avx2(const float* A, const float* x, float* y,
                 size_t rows, size_t cols) {
    for (size_t r = 0; r < rows; ++r) {
        __m256 vsum = _mm256_setzero_ps();
        size_t c = 0;

        for (; c + 8 <= cols; c += 8) {
            __m256 va = _mm256_loadu_ps(A + r * cols + c);
            __m256 vx = _mm256_loadu_ps(x + c);
            vsum = _mm256_fmadd_ps(va, vx, vsum);
        }

        float sum_arr[8];
        _mm256_storeu_ps(sum_arr, vsum);
        float sum = sum_arr[0] + sum_arr[1] + sum_arr[2] + sum_arr[3]
                 + sum_arr[4] + sum_arr[5] + sum_arr[6] + sum_arr[7];

        for (; c < cols; ++c) sum += A[r * cols + c] * x[c];
        y[r] = sum;
    }
}

AVX-512 간단 소개

AVX-512는 512비트 레지스터로 float 16개를 한 번에 처리합니다. 서버/워크스테이션 CPU에서 지원하며, _mm512_* 인트린직을 사용합니다.

아래 코드는 cpp를 사용한 구현 예제입니다. 필요한 모듈을 import하고, 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#if defined(__AVX512F__)
#include <immintrin.h>

void add_float16_avx512(const float* a, const float* b, float* c, size_t n) {
    size_t i = 0;
    for (; i + 16 <= n; i += 16) {
        __m512 va = _mm512_loadu_ps(a + i);
        __m512 vb = _mm512_loadu_ps(b + i);
        _mm512_storeu_ps(c + i, _mm512_add_ps(va, vb));
    }
    for (; i < n; ++i) c[i] = a[i] + b[i];
}
#endif

주의: AVX-512는 클럭 다운 가능. 벤치마크로 이득 확인 권장.


참고 자료


정리

항목설명
개념SIMD = 한 명령으로 여러 데이터 동시 처리
AVX2256비트, float 8개. loadu/storeu로 정렬 불필요
NEONARM 128비트, float 4개
자동 벡터화-O3, 연속 접근, 의존성 없음이 유리
에러정렬, 나머지 처리, CPU 기능 검사
프로덕션CPU 디스패처, 폴백 경로, SIMD 라이브러리

핵심 원칙:

  1. 먼저 자동 벡터화를 시도하고, 실패 시 인트린직 사용
  2. 정렬되지 않은 데이터는 loadu/storeu 사용
  3. 배열 크기가 벡터 크기의 배수가 아니면 나머지 스칼라 처리
  4. 배포 시 CPU 기능 검사 및 폴백 구현 제공

구현 체크리스트

  • -O3 -march=native (또는 구체적 타겟) 컴파일 옵션 설정
  • 정렬되지 않은 데이터는 _mm256_loadu_ps 사용
  • 나머지 원소 스칼라 처리
  • 프로덕션: CPU 기능 검사 및 폴백 경로
  • 벤치마크로 실제 환경에서 성능 측정
  • SoA 등 데이터 레이아웃 검토

자주 묻는 질문 (FAQ)

Q. 이 내용을 실무에서 언제 쓰나요?

A. 이미지/비디오 처리, 과학 계산, 게임 물리 엔진, 머신러닝 추론 등 대량 데이터 병렬 처리 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.

Q. 선행으로 읽으면 좋은 글은?

A. 각 글 하단의 이전 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.

Q. 더 깊이 공부하려면?

A. cppreference와 Intel Intrinsics Guide, ARM NEON 문서를 참고하세요. xsimd, Highway 같은 크로스 플랫폼 SIMD 라이브러리도 활용하면 좋습니다.

한 줄 요약: SSE·AVX2·NEON 인트린직으로 4배 빠르게를 마스터할 수 있습니다.


시리즈 네비게이션

이전 글: C++ 프로파일링 도구 마스터 #51-1 — perf·gprof·Valgrind·VTune·Tracy로 병목 찾기

다음 글: C++ 메모리 풀 고급 기법 #51-4 — 객체 풀·슬랩 할당자·메모리 아레나


관련 글

... 996 lines not shown ... Token usage: 63706/1000000; 936294 remaining Start-Sleep -Seconds 3