Skip to content

✍️ 필사 모드: SIMD / AVX / NEON 벡터화 Deep Dive — 인트린식, 자동 벡터화, simdjson, Highway, std::simd 완전 정복 (2025)

한국어
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.

TL;DR

  • SIMD (Single Instruction, Multiple Data): 하나의 명령어로 여러 데이터를 병렬 처리. CPU의 숨은 N배 성능.
  • 진화: MMX (1996) → SSE (1999) → AVX (2011) → AVX-512 (2016) → ARM NEON → SVE/SVE2. 256-bit AVX2가 현재 가장 널리 지원.
  • 핵심 아이디어: 256-bit 레지스터에 8 × int32 또는 4 × double 담기. 한 명령어가 8/4개를 동시 처리.
  • Intrinsics: _mm256_add_epi32(a, b)처럼 C/C++ 함수로 SIMD 명령어 직접 사용. 컴파일러가 네이티브 명령어 생성.
  • 자동 벡터화: 컴파일러가 루프를 자동으로 SIMD 변환. 하지만 쉽게 실패if 하나, pointer alias 하나가 defeating.
  • Data Layout: **SoA (Struct of Arrays)**가 SIMD 친화. AoS (Array of Structs)는 shuffle/gather 필요 → 느림.
  • simdjson: Daniel Lemire의 2019년 논문. JSON 파싱 4 GB/s 달성. SIMD의 대표 성과.
  • 포터블 SIMD: Google Highway, C++26 std::simd, Rust std::simd (nightly), WebAssembly SIMD.
  • 실전 응용: 압축(LZ4, zstd), 암호화(AES-NI, SHA-NI), 이미지/비디오, ML 추론, 해싱, 문자열 검색, 데이터베이스.

1. SIMD의 기본 아이디어

1.1 왜 필요한가

CPU는 점점 빨라지지 않는다. 클록 속도는 2005년 이후 거의 정체 (~3-5 GHz). 성능 향상의 경로:

  1. 멀티 코어 (병렬).
  2. ILP (Instruction-Level Parallelism, out-of-order 실행).
  3. SIMD (Data-Level Parallelism).
  4. 캐시 확장.

SIMD는 "같은 일을 여러 번" 할 때 가장 효과적. 다행히 현대 워크로드(이미지, 비디오, ML, 데이터베이스, 압축)는 대부분 이 패턴.

1.2 기본 개념

SISD (Single Instruction, Single Data): 전통적 스칼라 연산.

add r1, r2  → r1 = r1 + r2  (한 값만)

SIMD (Single Instruction, Multiple Data):

vpaddd ymm0, ymm1, ymm2
→ ymm0 = [ymm1[0]+ymm2[0], ymm1[1]+ymm2[1], ..., ymm1[7]+ymm2[7]]

256-bit 레지스터에 8 × int32. 한 명령어로 8개 덧셈.

1.3 속도 향상

이론적 최대:

  • SSE (128-bit): 4 × int32 → 4배.
  • AVX2 (256-bit): 8 × int32 → 8배.
  • AVX-512 (512-bit): 16 × int32 → 16배.

실제는 메모리 대역폭데이터 의존성이 제한. 하지만 많은 워크로드에서 5-10배 성능 향상.


2. 역사와 진화

2.1 MMX (1996)

Intel Pentium MMX. "MultiMedia eXtensions".

  • 64-bit 레지스터 (mm0-mm7).
  • 정수만 (int8, int16, int32).
  • FPU 레지스터와 공유 → FPU 사용과 혼용 불가.

멀티미디어(MPEG 디코딩, 이미지) 가속이 목표. 제한적이었지만 SIMD 시대의 시작.

2.2 SSE (1999)

Streaming SIMD Extensions.

  • 128-bit 레지스터 (xmm0-xmm15).
  • float 지원 (4 × float).
  • FPU와 독립 → 자유로운 사용.

SSE1부터 SSE4까지 여러 세대:

  • SSE1: float 기본.
  • SSE2 (2000): double, int 지원 → 사실상 표준.
  • SSE3, SSSE3, SSE4.1, SSE4.2: 추가 명령어 (horizontal add, dot product, string ops).

x86-64 요구사항에 SSE2 포함 → 모든 64-bit x86에 있음.

2.3 AVX (2011)

Advanced Vector Extensions. Sandy Bridge부터.

  • 256-bit 레지스터 (ymm0-ymm15).
  • float/double 기본.
  • 3-operand 형식 (destructive 아님).

AVX2 (2013): 정수 256-bit 확장. 현재 가장 널리 지원 — 2013+ 거의 모든 Intel/AMD.

2.4 FMA (Fused Multiply-Add)

r = a × b + c

2 명령어가 1 명령어에. 정확도도 향상 (중간 rounding 없음). AVX2와 함께 보급.

2.5 AVX-512 (2016)

512-bit 레지스터 (zmm0-zmm31). 32개 레지스터.

  • 16 × int32 또는 8 × double.
  • Mask 레지스터 (k0-k7) — 조건부 실행.
  • Gather/scatter 개선.
  • Conflict detection — 병렬 해시맵 업데이트.

복잡한 명령어 체계. 여러 "sub-sets":

  • AVX-512F (Foundation).
  • AVX-512CD (Conflict Detection).
  • AVX-512BW (Byte/Word).
  • ... 수십 개.

문제: CPU마다 다른 subset 지원. 프로그래머에게 혼란. Intel 서버/워크스테이션에만 주로. 2019년 Intel이 일부 consumer CPU에서 disable.

2.6 ARM NEON

ARM의 SIMD. ARMv7 (2004)부터.

  • 128-bit 레지스터 (v0-v31).
  • float, double, int.
  • x86 SSE와 유사한 수준.

AArch64 (64-bit ARM): NEON이 항상 존재. Apple Silicon, Cortex-A, Graviton 등 모든 서버/모바일 ARM에 내장.

2.7 SVE / SVE2

Scalable Vector Extension. ARM의 AVX-512 대응 (2016+).

독특한 점: 벡터 길이가 런타임에 결정. 128-2048 bit. 같은 코드가 다른 CPU에서 다른 폭으로 동작.

  • Cortex-X2: 128-bit.
  • Fujitsu A64FX (富岳 슈퍼컴퓨터): 512-bit.
  • 향후 CPU: 더 넓어질 것.

Amazon Graviton 3 (2022): SVE 지원. 서버 시장에서 점차 등장.

2.8 RISC-V V

RISC-V의 Vector extension (RVV). 2021년 1.0 ratified. SVE와 유사한 "scalable" 접근. 미래의 RISC-V 서버/ML 칩 핵심.

2.9 GPU SIMT와의 차이

이 세션 CUDA 포스트에서 다룬 SIMT: GPU가 스칼라 코드를 32 threads warp로 실행.

항목CPU SIMDGPU SIMT
모델명시적 벡터스칼라처럼
4-1632 (NVIDIA)
분기수동 masking자동 (divergence 대가)
코드 작성어려움쉬움
메모리좁은 것만광대한 HBM

둘 다 데이터 병렬성을 활용하지만 접근이 다르다.


3. 명령어 타입

3.1 Arithmetic

Vertical ops — 같은 위치의 요소끼리:

[a0, a1, a2, a3] + [b0, b1, b2, b3] = [a0+b0, a1+b1, a2+b2, a3+b3]
  • _mm256_add_epi32(a, b): int32 8개 덧셈.
  • _mm256_mul_ps(a, b): float 8개 곱셈.
  • _mm256_fmadd_ps(a, b, c): a*b + c, fused.
  • _mm256_sub_epi16(a, b): int16 16개 뺄셈.

3.2 Horizontal ops

한 레지스터 내부:

[a0, a1, a2, a3, a4, a5, a6, a7] → a0+a1+a2+a3+a4+a5+a6+a7
  • _mm256_hadd_ps(a, b): horizontal add pairs.
  • Reduction을 위해 여러 step 필요.

Horizontal은 일반적으로 느리고 제한적. 가능하면 vertical만 사용.

3.3 Shuffle / Permute

레지스터 내부 요소 재배열:

[a0, a1, a2, a3] with mask [2, 0, 3, 1]
[a2, a0, a3, a1]
  • _mm256_shuffle_epi32(a, imm): 각 128-bit lane 내 shuffle.
  • _mm256_permute2x128_si256(a, b, imm): 256-bit 간 lane 이동.
  • _mm256_permutevar8x32_epi32(a, idx): 임의의 순서.

복잡하지만 필수 (예: 행렬 전치, 복수 포인터 접근).

3.4 Blend / Select

조건에 따라 두 레지스터 중 선택:

mask = [1, 0, 1, 0, 1, 0, 1, 0]
blend(a, b, mask) = [a0, b1, a2, b3, a4, b5, a6, b7]
  • _mm256_blendv_epi8(a, b, mask): byte mask.
  • _mm256_max_epi32(a, b): 각 위치별 max.

조건 분기의 SIMD 버전. Branchless 코드 작성에 핵심.

3.5 Compare

두 벡터 비교 → mask:

[1, 5, 3, 7] > [4, 2, 6, 1][0, -1, 0, -1]  (-1 = all bits set)
  • _mm256_cmpgt_epi32(a, b): greater than.
  • _mm256_cmpeq_epi32(a, b): equal.

Mask로 filter, blend, movemask 조합 가능.

3.6 Movemask

각 요소의 MSB (가장 높은 비트)를 모아 정수로:

[0, -1, 0, -1, -1, 0, -1, 0]0b01011010
  • _mm256_movemask_epi8(a): byte별 → 32-bit mask.

Compare 결과 → movemask → 비트 순회로 매칭 요소 찾기. Swiss Table의 핵심 기법.

3.7 Gather / Scatter

비연속 메모리 접근:

gather(base, [3, 1, 7, 2]) = [base[3], base[1], base[7], base[2]]
  • _mm256_i32gather_epi32(base, idx, scale).
  • Scatter는 AVX-512에만.

느림 (여전히 N 번 메모리 접근). 하지만 루프보다는 빠름.


4. Intrinsics 작성

4.1 기본 예제

C로 배열 덧셈:

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

SIMD 버전:

#include <immintrin.h>

void add_avx2(int* a, int* b, int* c, int n) {
    int i = 0;
    for (; i <= n - 8; i += 8) {
        __m256i va = _mm256_loadu_si256((__m256i*)(a + i));
        __m256i vb = _mm256_loadu_si256((__m256i*)(b + i));
        __m256i vc = _mm256_add_epi32(va, vb);
        _mm256_storeu_si256((__m256i*)(c + i), vc);
    }
    // Tail handling
    for (; i < n; i++) {
        c[i] = a[i] + b[i];
    }
}

~8배 빠름 (메모리 대역폭 제한 이전).

4.2 네이밍 규칙

Intel intrinsics 이름 구조:

_mm{RegWidth}_{Operation}_{Type}

예:

  • _mm_add_epi32: 128-bit (SSE), add, 32-bit 정수.
  • _mm256_add_epi32: 256-bit (AVX), add, 32-bit 정수.
  • _mm512_add_epi32: 512-bit (AVX-512).
  • _mm256_mul_ps: 256-bit, multiply, packed single (float).
  • _mm256_mul_pd: packed double.

데이터 타입:

  • epi8, epi16, epi32, epi64: signed integer.
  • epu8, epu16, ...: unsigned.
  • ps: packed single (float).
  • pd: packed double.

4.3 Loadu vs Load

_mm256_loadu_si256: unaligned load. 어느 주소든. _mm256_load_si256: aligned load. 32-byte 경계 필수 (아니면 crash).

과거엔 aligned가 더 빨랐지만 현대 CPU는 거의 차이 없음. loadu 사용 권장.

4.4 Alignment

대량 데이터는 정렬하면 약간 더 빠름:

alignas(32) int buffer[1024];
// 또는
int* buf = (int*)aligned_alloc(32, 1024 * sizeof(int));

Rust의 경우 #[repr(align(32))].

4.5 Header 파일

#include <mmintrin.h>   // MMX
#include <xmmintrin.h>  // SSE
#include <emmintrin.h>  // SSE2
#include <pmmintrin.h>  // SSE3
#include <tmmintrin.h>  // SSSE3
#include <smmintrin.h>  // SSE4.1
#include <nmmintrin.h>  // SSE4.2
#include <immintrin.h>  // AVX / AVX2 / AVX-512 전부

일반적으로 <immintrin.h> 하나면 됨.

4.6 컴파일

gcc -mavx2 -mfma -O3 prog.c

-mavx2: AVX2 사용 허용. -mfma: FMA. -march=native: 현재 CPU의 모든 기능 사용.

주의: -march=native로 빌드한 바이너리는 해당 CPU에서만 실행.


5. 자동 벡터화

5.1 컴파일러가 시도

현대 GCC/Clang/MSVC는 간단한 루프를 자동으로 SIMD 변환:

void add(int* a, int* b, int* c, int n) {
    for (int i = 0; i < n; i++) {
        c[i] = a[i] + b[i];
    }
}

-O3 컴파일 시 AVX2 코드 생성. 확인:

gcc -O3 -mavx2 -fopt-info-vec prog.c
# analysis: vectorized loop at line ...

5.2 왜 실패하는가

자동 벡터화는 매우 fragile:

Pointer aliasing:

void add(int* a, int* b, int* c, int n) {
    // a, b, c가 같은 메모리를 가리킬 수 있음 → 안전하지 않음
}

해결: restrict (C) 또는 template (C++).

분기:

for (int i = 0; i < n; i++) {
    if (a[i] > 0) {
        c[i] = a[i] * 2;
    }
}

조건부는 벡터화 어려움. Branchless 재작성 필요.

함수 호출:

for (int i = 0; i < n; i++) {
    c[i] = sqrt(a[i]);  // sqrt가 인라인되지 않으면 실패
}

__builtin_sqrt 또는 <math.h>의 intrinsic 버전 필요.

Complex data dependencies:

for (int i = 1; i < n; i++) {
    a[i] = a[i-1] + 1;  // 직전 값 의존 → 순차
}

Non-unit stride:

for (int i = 0; i < n; i += 7) { ... }

보통 가능하지만 최적은 아님.

5.3 Vectorization Report

gcc -O3 -fopt-info-vec-all prog.c

각 루프에 대해 "vectorized" 또는 "not vectorized because..." 출력. 왜 실패했는지 진단.

5.4 Pragmas

컴파일러 힌트:

#pragma GCC ivdep  // no dependencies
for (int i = 0; i < n; i++) { ... }

#pragma omp simd  // OpenMP SIMD
for (int i = 0; i < n; i++) { ... }

5.5 명시적 제어 vs 자동

  • 자동: 간단, 포터블. 하지만 예측 불가.
  • 수동 (intrinsics): 확실한 SIMD, 하지만 플랫폼 종속, 읽기 어려움.

성능이 중요하면 수동. 편의를 원하면 자동.


6. Data Layout

6.1 AoS vs SoA

AoS (Array of Structs) — 자연스러운 객체 지향:

struct Particle {
    float x, y, z;
    float vx, vy, vz;
};
Particle particles[1000];

메모리:

[p0.x, p0.y, p0.z, p0.vx, p0.vy, p0.vz, p1.x, p1.y, ...]

SoA (Struct of Arrays) — SIMD 친화:

struct Particles {
    float x[1000], y[1000], z[1000];
    float vx[1000], vy[1000], vz[1000];
};

메모리:

x:  [x0, x1, x2, ..., x999]
y:  [y0, y1, y2, ..., y999]
...

6.2 왜 SoA가 빠른가

"모든 파티클의 x 좌표를 업데이트" 연산:

AoS:

x0  (skip vy, vz, ...) → x1  (skip) → x2 → ...

SIMD 로드에 shuffle 필요 (gather 또는 여러 load + blend).

SoA:

x0, x1, x2, x3, x4, x5, x6, x7 → 한 레지스터에

단순 loadu. 8개가 연속 메모리.

차이: 10-100배.

6.3 AoSoA — 타협

한 그룹(예: 8개)만 SoA, 그룹 사이엔 AoS:

struct Particles8 {
    float x[8], y[8], z[8];
    float vx[8], vy[8], vz[8];
};
Particles8 particles[125];  // 125 groups × 8 = 1000
  • 캐시 친화 (한 그룹이 같은 캐시 라인).
  • SIMD 로딩 쉬움.
  • 그룹 간 이동 비용 약간 있음.

게임 엔진(Unity DOTS), ECS에서 흔함.

6.4 Column Store

DB의 컬럼나 저장 (ClickHouse, DuckDB, Parquet)도 같은 원리. 각 컬럼이 별도 배열 → SIMD 분석 쿼리에 최적.


7. simdjson — SIMD의 걸작

7.1 배경

JSON 파싱은 웹 서비스의 hidden bottleneck. 대규모 API, 로그 수집기, ETL 파이프라인에서 파싱에 CPU의 절반 이상.

전통 파서 (rapidjson, jansson): 수백 MB/s. 여전히 캐릭터별 스캔.

7.2 Daniel Lemire의 논문 (2019)

"Parsing Gigabytes of JSON per Second" — simdjson 프로젝트 발표. 4+ GB/s. 기존 대비 10배 이상.

7.3 핵심 기법

1. 구조 파악을 SIMD로:

JSON 문자열을 한 번에 스캔해서 structural characters(, [], ',', ':')의 위치를 비트맵으로.

__m256i input = _mm256_loadu_si256(...);
__m256i cmp_brace = _mm256_cmpeq_epi8(input, _mm256_set1_epi8('{'));
// ... compare with other structural chars ...
uint32_t brace_mask = _mm256_movemask_epi8(cmp_brace);

32 바이트를 한 번에 분석.

2. Character class lookup:

각 바이트가 어떤 종류인지 (whitespace, structural, string content) 판단. SIMD lookup table:

__m256i lookup = _mm256_setr_epi8(/* class per byte */);
__m256i classes = _mm256_shuffle_epi8(lookup, input);

32 바이트 분류를 한 instruction.

3. 병렬 bitmap 처리:

Structural mask를 64-bit으로 변환 → __builtin_ctzll로 다음 구조 문자 위치 찾기 (1 cycle).

4. UTF-8 검증:

Unicode valid한지 확인을 SIMD로. 복잡한 state machine을 vector로.

7.4 단계

simdjson의 파이프라인:

  1. Stage 1: SIMD로 structural mask 생성.
  2. Stage 2: Tape 만들기 (token 배열).
  3. Stage 3: DOM 트리 생성 (옵션).

Stage 1이 대부분 SIMD로 커버 → 가장 빠른 단계.

7.5 생태계

simdjson은 오픈소스. 많은 프로젝트 채택:

  • ClickHouse: JSON 컬럼.
  • Node.js: 일부 경로.
  • PHP: 표준 json_decode의 대안.
  • Go, Rust, Python 바인딩.

"SIMD 기반 고성능 라이브러리의 모범 사례".


8. 응용 분야

8.1 압축

LZ4, Zstandard, Snappy 모두 SIMD 활용:

  • 매치 길이 찾기.
  • 해시 값 계산.
  • 압축 해제 시 복사.

zstd는 SIMD 최적화로 초당 수 GB 압축 해제.

8.2 암호화

AES-NI: Intel/AMD의 AES 전용 명령어. 일반 AES 대비 10배 빠름. SHA-NI: SHA-256 전용. PCLMULQDQ: Carry-less 곱셈 (GCM에 필수).

현대 HTTPS가 CPU의 1% 미만만 쓰는 이유.

8.3 문자열 처리

strlen, strcmp, memchr, strstr: libc가 SIMD 구현.

단어 count, CSV 파싱, regex 매칭: SIMD로 가속 가능.

Intel의 Hyperscan: SIMD 기반 고성능 regex 엔진. 수천 패턴 동시 매칭.

8.4 이미지 / 비디오

JPEG/PNG 디코딩: 수평 필터, DCT가 SIMD. H.264/H.265 디코딩: 매크로블록 처리. libvpx, libav: 내부적으로 수만 줄의 SIMD 코드.

모바일에서는 NEON 필수 — 배터리 효율.

8.5 ML 추론

OpenBLAS, MKL: 행렬 곱 SIMD. ONNX Runtime: CPU backend는 SIMD 활용. llama.cpp: LLM CPU 추론. GGML이 AVX, AVX2, AVX-512, NEON 모두 지원.

GPU 없는 환경에서 LLM을 돌리는 것이 가능해진 이유.

8.6 데이터베이스

ClickHouse: 컬럼 연산, hash aggregation 모두 SIMD. DuckDB: Vector-based execution + SIMD. Postgres: 일부 custom extension (pg_embedding 등). SQLite: 일부 sort, scan.

8.7 해시맵 (이전 포스트)

Swiss Table이 SIMD로 16 슬롯 동시 비교. 현대 해시맵의 핵심.

8.8 예술적 응용

Game physics: 파티클 시뮬레이션, collision detection. Audio DSP: FFT, 필터, effects. Ray tracing: BVH traversal, 4/8 ray 병렬.


9. 포터블 SIMD

9.1 문제

Intrinsics는 플랫폼 종속:

  • x86: _mm_*, _mm256_*.
  • ARM: vadd_*, vld1q_*.
  • 완전히 다른 API.

Cross-platform 코드 작성 어려움. 여러 플랫폼 지원하려면 각자 구현 + #ifdef:

#if defined(__AVX2__)
    // x86 intrinsics
#elif defined(__ARM_NEON)
    // NEON intrinsics
#else
    // scalar fallback
#endif

유지보수 악몽.

9.2 Google Highway

2021년 Google이 공개. "Production-quality, portable SIMD".

#include <hwy/highway.h>
using namespace hwy::HWY_NAMESPACE;

template <class D>
void Add(D d, const float* a, const float* b, float* c, size_t n) {
    for (size_t i = 0; i < n; i += Lanes(d)) {
        auto va = Load(d, a + i);
        auto vb = Load(d, b + i);
        auto vc = Add(va, vb);
        Store(vc, d, c + i);
    }
}

DScalableTag — 하드웨어에 따라 크기 결정. x86-AVX2면 8, NEON이면 4, AVX-512면 16.

동적 dispatch: 런타임에 CPU 감지해 최적 버전 호출.

9.3 C++26 std::simd

C++26에 표준 SIMD 들어옴. 기반: Vir Christoph의 experimental design.

#include <simd>

std::simd<int> a = {1, 2, 3, 4, 5, 6, 7, 8};
std::simd<int> b = {8, 7, 6, 5, 4, 3, 2, 1};
auto c = a + b;  // vectorized

GCC 14+ 실험적 지원. 언어 표준이라 포팅 가능.

9.4 Rust std::simd

Rust의 portable SIMD (nightly):

use std::simd::*;

let a = i32x8::from_array([1,2,3,4,5,6,7,8]);
let b = i32x8::from_array([8,7,6,5,4,3,2,1]);
let c = a + b;

Rust 안정화 진행 중. Polars, simd-json (Rust port) 등이 채택.

9.5 WebAssembly SIMD

브라우저에서도 SIMD!

#include <wasm_simd128.h>

v128_t a = wasm_v128_load(ptr_a);
v128_t b = wasm_v128_load(ptr_b);
v128_t c = wasm_i32x4_add(a, b);

128-bit fixed width. Chrome, Firefox, Safari 모두 지원.

사용처:

  • ffmpeg.wasm (비디오 변환).
  • sql.js (SQLite WASM).
  • Llama.cpp WASM 포트.
  • Browser 기반 ML 추론.

10. AVX-512 논쟁

10.1 강력하지만 복잡

AVX-512는 많은 것을 제공:

  • 16 × int32 / 8 × double.
  • Mask 레지스터.
  • 32 레지스터 (AVX2의 16).
  • 새 명령어 (VPCLMULQDQ, etc).

10.2 Subset 지옥

여러 subset이 있음:

  • AVX-512F: Foundation.
  • AVX-512CD: Conflict Detection.
  • AVX-512BW: Byte/Word.
  • AVX-512VL: Vector Length (256/128-bit에도 AVX-512 연산).
  • AVX-512VNNI: Neural Network Instructions.
  • AVX-512IFMA: Integer FMA.

CPU마다 지원 조합이 다름. 프로그래머가 어느 subset을 요구할지 결정.

10.3 Frequency Throttling

Intel AVX-512가 클록을 낮춘다. 이유: 넓은 레지스터 사용이 전력 소모 증가 → thermal 한계 → 주파수 하락.

짧은 벡터 작업 후 스칼라 코드가 느려진 채로 실행. "AVX-512를 쓰면 오히려 느려질 수 있다"는 역설.

Intel은 이후 CPU에서 개선했지만 논란.

10.4 Consumer CPU에서 disable

Intel Alder Lake (2021, 12th gen): E-core가 AVX-512 미지원 → 일부 초기 CPU에서 BIOS로 disable 가능, 이후 완전 제거.

AMD Zen 4 (2022): AVX-512 full 지원. 두 개의 256-bit 유닛으로 구현(double-pumped) → 성능은 Intel만 못하지만 호환성.

Intel Sapphire Rapids (2023): 서버만 AVX-512. Consumer는 AVX2까지.

결과: AVX-512는 서버/HPC 한정. 일반 데스크톱 코드는 AVX2가 상한.

10.5 AVX10

Intel의 새 제안 (2023). "AVX-512의 정리판":

  • 더 단순한 subset.
  • Consumer + server 모두.
  • 향후 표준으로.

2025년 현재 초기 단계. AMD 동참 여부 불투명.


11. 실전 튜닝

11.1 측정 먼저

perf stat -e cycles,instructions,cache-misses ./program

병목을 확인. SIMD 최적화는 compute-bound 워크로드에만 효과. Memory-bound면 대역폭이 한계.

11.2 Hot Loop 찾기

perf record -g ./program
perf report

가장 많은 시간을 쓰는 함수. 일반적으로 루프 몇 개가 95%.

11.3 자동 벡터화 우선

처음엔 컴파일러에 맡기기:

gcc -O3 -march=native -fopt-info-vec ./code.c

성공하면 read-only. 실패하면 수동 작성.

11.4 수동 SIMD 패턴

Pattern 1: 단순 element-wise

for (int i = 0; i < n; i += 8) {
    __m256 a = _mm256_loadu_ps(A + i);
    __m256 b = _mm256_loadu_ps(B + i);
    __m256 c = _mm256_mul_ps(a, b);
    _mm256_storeu_ps(C + i, c);
}

Pattern 2: Reduction

__m256 sum = _mm256_setzero_ps();
for (int i = 0; i < n; i += 8) {
    __m256 v = _mm256_loadu_ps(data + i);
    sum = _mm256_add_ps(sum, v);
}
// Horizontal sum
// ...

Pattern 3: Masked

for (int i = 0; i < n; i += 8) {
    __m256 v = _mm256_loadu_ps(data + i);
    __m256 mask = _mm256_cmp_ps(v, threshold, _CMP_GT_OQ);
    __m256 result = _mm256_blendv_ps(v, zero, mask);
    _mm256_storeu_ps(out + i, result);
}

11.5 벤치마크

Google benchmark, Criterion (Rust), Hyperfine:

static void BM_ScalarAdd(benchmark::State& state) {
    // ...
    for (auto _ : state) {
        add_scalar(a, b, c, n);
    }
}
BENCHMARK(BM_ScalarAdd);

static void BM_SimdAdd(benchmark::State& state) {
    // ...
    for (auto _ : state) {
        add_avx2(a, b, c, n);
    }
}
BENCHMARK(BM_SimdAdd);

정확한 비교. 워밍업, statistical 분석.

11.6 CPU 감지

런타임에 CPU 기능 감지:

if (__builtin_cpu_supports("avx2")) {
    use_avx2();
} else if (__builtin_cpu_supports("sse4.1")) {
    use_sse4();
} else {
    use_scalar();
}

Function multi-versioning (GCC/Clang):

__attribute__((target_clones("default,avx,avx2,avx512f")))
int my_function(int* a, int n) {
    // 컴파일러가 여러 버전 생성
}

12. 학습 리소스

공식:

:

  • "Modern x86 Assembly Language Programming" — Daniel Kusswurm.
  • "Computer Systems: A Programmer's Perspective" — Bryant & O'Hallaron.

블로그:

  • Wojciech Muła (simdjson 공동 저자).
  • Daniel Lemire (simdjson 저자).
  • Intel의 "Optimization Notes".

실습:

  • Godbolt Compiler Explorer: Intrinsics 바로 실험.
  • Highway GitHub 예제.
  • simdjson 소스 코드.

영상:

  • Chandler Carruth의 CppCon talks.
  • Daniel Lemire의 발표들.

13. 요약 — 한 장 정리

┌─────────────────────────────────────────────────────┐
SIMD Cheat Sheet├─────────────────────────────────────────────────────┤
│ 진화:MMX (1996)SSE (1999)AVX (2011)│   → AVX2 (2013)AVX-512 (2016)ARM: NEONSVE / SVE2│                                                       │
│ 레지스터 폭:SSE: 128-bit (4 × int32 / 4 × float)AVX: 256-bit (8 × int32 / 8 × float)AVX-512: 512-bit (16 × int32)NEON: 128-bit                                     │
SVE: 128 ~ 2048-bit (scalable)│                                                       │
│ 주요 연산:Vertical: add, sub, mul, fma                       │
Horizontal: hadd, reduce                          │
Shuffle / permute                                  │
Blend / select                                    │
Compare + movemask                                │
Gather / scatter                                   │
│                                                       │
Intrinsics 네이밍:│   _mm{width}_{op}_{type}│   width: 없음=128, 256, 512│   type: epi8/16/32/64, ps, pd                       │
│                                                       │
Data Layout:SoA > AoS for SIMDAoSoA for cache + SIMDColumn store                                       │
│                                                       │
│ 자동 벡터화:GCC/Clang: -O3 -march=native                      │
│   확인: -fopt-info-vec                               │
│   방해 요인: alias, branch, call, dep               │
│                                                       │
│ 포터블 SIMD:Google HighwayC++26 std::simd                                   │
Rust std::simd (nightly)WebAssembly SIMD (128-bit)│                                                       │
│ 유명 활용:simdjson (4 GB/s JSON)AES-NI, SHA-NI, PCLMULQDQSwiss Table (hash map)ClickHouse, DuckDB vectorization                  │
│   llama.cpp GGML (CPU LLM)FFmpeg, x264 / x265                                │
│                                                       │
AVX-512 주의:Subset 복잡                                          │
Frequency throttling                               │
Consumer CPU에서 제거됨                              │
AMD Zen 4 / Intel 서버만                           │
│                                                       │
│ 튜닝 워크플로우:1. perf로 병목 찾기                                 │
2. 자동 벡터화 시도                                  │
3. 실패 시 intrinsics                               │
4. 벤치마크                                          │
5. CPU multi-versioning                            │
└─────────────────────────────────────────────────────┘

14. 퀴즈

Q1. SoA가 SIMD에서 AoS보다 빠른 이유는?

A. SIMD는 연속된 메모리를 한 번에 로드하는데, SoA는 같은 필드가 연속 배열이므로 직접 로드 가능. "파티클의 x 좌표를 업데이트" 연산: AoS는 [x0, y0, z0, vx0, ...] 레이아웃이라 x만 모으려면 shuffle/gather 필요 — SIMD는 가능하지만 5-10배 느림. SoA는 [x0, x1, x2, x3, x4, x5, x6, x7]이 그대로 한 AVX2 레지스터에 로드 → 단일 _mm256_loadu_ps. 이 차이가 게임 엔진이 ECS로, 분석 DB가 컬럼 저장으로 이동한 이유. "객체 지향 클래스로 예쁜 코드 vs SIMD로 10배 빠른 코드"의 선택. AoSoA는 타협 — 8개씩 SoA 그룹, 그룹 사이엔 AoS — 캐시와 SIMD 둘 다 친화.

Q2. 자동 벡터화가 실패하는 흔한 이유는?

A. Pointer aliasing, 분기, 함수 호출, 데이터 의존성 네 가지. (1) Aliasing: 컴파일러가 a, b, c 포인터가 겹치는지 모르면 벡터화 불안전 — restrict 키워드 또는 C++ template으로 해결. (2) 분기: 루프 안의 if는 SIMD로 표현 어려움 — branchless 재작성 또는 mask 연산. (3) 함수 호출: sqrt 같은 함수가 인라인되지 않으면 벡터화 불가 — intrinsic 버전 사용. (4) Loop-carried dependency: a[i] = a[i-1] + 1 같은 순차 의존 — 수학적으로 불가능. gcc -O3 -fopt-info-vec-all로 어느 루프가 실패했는지 확인 가능. 자동 벡터화는 fragile하므로 성능 중요 코드는 intrinsics로 명시하거나 Highway/std::simd 같은 포터블 추상화 사용. "컴파일러가 알아서 해줄 것"이라는 가정은 위험 — 한 줄의 수정이 조용히 벡터화를 제거할 수 있다.

Q3. simdjson이 JSON 파싱을 4 GB/s로 만든 방법은?

A. Structural character detection을 SIMD 32-byte 단위로 스캔. 전통 JSON 파서는 바이트별로 "이게 {인가, }인가, "인가?" 확인 → O(n) 스칼라 순회. simdjson (Daniel Lemire, 2019): (1) 32 바이트를 AVX2 레지스터에 로드, (2) _mm256_cmpeq_epi8각 바이트가 특정 문자인지 한 명령어로 검사, (3) _mm256_movemask_epi8로 결과를 32-bit mask로 압축, (4) bitmask 연산으로 structural 위치, string 범위, whitespace 등을 병렬 식별. 추가로 character class lookup table로 "이 바이트는 숫자/문자/연산자/공백"을 SIMD shuffle로 분류. UTF-8 validation도 state machine을 SIMD로. 스칼라 rapidjson(~200 MB/s) 대비 20배 빠름. simdjson 논문 "Parsing Gigabytes of JSON per Second"는 SIMD 활용의 교과서 사례. 이후 많은 언어(Rust simd-json, Go, Python)가 포트. "CPU에 이미 있는 능력을 제대로 쓰면 10배 성능"이 가능하다는 증명.

Q4. AVX-512가 Intel consumer CPU에서 제거된 이유는?

A. Frequency throttling과 복잡도. (1) Throttling: AVX-512는 넓은 레지스터(512-bit) 사용으로 전력 소모 급증 → 열 한계 → CPU가 자동으로 주파수를 낮춤. 짧은 AVX-512 작업 후 스칼라 코드가 느려진 상태로 실행 → 의도치 않은 성능 저하. "AVX-512 쓰면 오히려 느려진다"는 역설. (2) Subset 복잡도: AVX-512F/CD/BW/VL/VNNI/IFMA 등 수십 개 subset, CPU마다 지원 조합이 달라 프로그래머에 혼란. (3) Alder Lake (2021)의 P-core/E-core: 효율 코어(E-core)가 AVX-512 미지원 → 전체 CPU에서 사용 못함. Intel은 결국 consumer CPU(12th gen 이후)에서 완전 제거. AMD Zen 4 (2022)는 AVX-512 지원 — 두 256-bit 유닛으로 구현(double-pumped). Intel Sapphire Rapids (서버)만 AVX-512 유지. 결과: AVX-512는 서버/HPC 한정, 일반 데스크톱 코드는 AVX2가 최대. Intel의 AVX10 제안(2023)이 이를 정리하려 하지만 아직 초기. 강력한 기술도 생태계/복잡도 문제로 제한될 수 있다는 교훈.

Q5. ARM NEON과 x86 AVX2의 차이는?

A. 폭, 명령어 스타일, 확장 방향. (1) : NEON은 128-bit 고정 (AVX2의 256-bit 절반). AArch64의 SVE/SVE2가 가변 폭(128-2048)으로 확장 담당. (2) 명령어 스타일: NEON intrinsics는 vadd_s32, vld1q_f32처럼 타입이 이름에 포함. AVX는 _mm256_add_epi32처럼 비트 폭과 타입 분리. (3) 3-operand: NEON은 처음부터 destructive 아님, AVX가 AVX1부터 3-operand 도입. (4) FMA: NEON은 vmla_f32(multiply-accumulate)가 초기부터, AVX는 AVX2+FMA에서 분리 도입. 실용 차이: ARM은 모바일/배터리 중심 — NEON 128-bit도 Apple Silicon에서 수 GB/s 처리. Intel/AMD는 서버/워크스테이션 — 넓은 레지스터로 피크 성능. Apple M1/M2/M3/M4는 NEON으로 llama.cpp, FFmpeg, Photoshop을 충분히 빠르게 돌림 — 폭보다 메모리 대역폭(unified memory)과 효율이 중요. 2025년 현재 "x86이 절대적으로 빠르다"는 더 이상 사실이 아님. Graviton 3(SVE 256-bit) 같은 서버 ARM이 Intel/AMD와 경쟁.

Q6. SIMD 코드에서 "horizontal operation"이 느린 이유는?

A. 레지스터 내부 요소 간 통신이 하드웨어 수준에서 어려움. Vertical op(요소별 대응 연산)는 각 레인이 독립적 → 병렬. Horizontal op(예: a[0]+a[1]+a[2]+...+a[7])은 한 레지스터 안의 값들을 합산 → 레인 간 데이터 이동 필요. SIMD 하드웨어는 "각 레인이 독립 ALU"로 설계되어 있어 레인 간 통신이 추가 cycle. _mm256_hadd_ps는 pairs 병합이라 full reduction에 3 step 필요. 일반적으로 horizontal 최소화, vertical 최대화가 SIMD 튜닝 원칙. 패턴: reduction을 만드는 법 — SIMD로 부분 합을 누적한 후 마지막에만 horizontal. 예: sum 루프에서 sum = _mm256_add_ps(sum, v)를 반복 → 마지막에 한 번 horizontal sum. 이러면 99%의 compute는 vertical, 마지막 1%만 horizontal. GPU는 이 문제에 더 잘 대응(warp shuffle, reduce primitive) — CPU SIMD의 고전적 한계.

Q7. C++26 std::simd나 Google Highway가 필요한 이유는?

A. 포터블 SIMD 작성과 런타임 dispatch. 전통적 접근은 #ifdef __AVX2__ / __ARM_NEON__ / ...로 여러 구현 + 컴파일 시 CPU 고정 → 유지보수 악몽, 한 바이너리가 다양한 CPU에 대응 불가. Highway: (1) 단일 코드Add(va, vb) 같은 추상화가 x86/ARM/WASM/SVE 모두에 동작. (2) Scalable tagScalableTag<int32_t> d가 하드웨어에 맞는 폭 자동 선택 (128-bit이면 4, 256이면 8, 512면 16). (3) 동적 dispatch — 런타임에 CPU 감지해 최적 버전 선택, 단일 바이너리가 여러 CPU 대응. (4) Production quality — Google이 Chrome, Highway, JPEG XL에 사용 중. C++26 std::simd: 언어 표준이라 컴파일러가 직접 지원, 외부 라이브러리 불필요. Rust std::simd, Swift SIMD도 같은 목적. 이점: (a) 코드 1번 작성, (b) 여러 CPU 지원, (c) 새 ISA 자동 지원, (d) 가독성 — intrinsics 이름을 외울 필요 없음. 단점: 약간의 추상화 오버헤드(보통 무시 가능), 최저수준 튜닝 어려움. 대부분 애플리케이션은 Highway/std::simd로 충분.


15. 학습 리소스 및 결론

공식 자료:

  • Intel Intrinsics Guide (웹 검색).
  • Agner Fog의 microarchitecture manuals.
  • ARM NEON Programmer's Guide.

프로젝트:

  • simdjson GitHub.
  • Highway GitHub.
  • llama.cpp GGML source.

관련 포스트:

  • "CUDA GPU Programming" — GPU의 SIMT.
  • "ClickHouse Internals" — 서버 DB의 SIMD.
  • "Hash Map Internals" — Swiss Table의 SIMD lookup.
  • "CPU Cache & Memory Hierarchy" — 데이터 배치 최적화.

현재 단락 (1/550)

- **SIMD** (Single Instruction, Multiple Data): 하나의 명령어로 여러 데이터를 병렬 처리. CPU의 숨은 N배 성능.

작성 글자: 0원문 글자: 20,156작성 단락: 0/550