필사 모드: SIMD / AVX / NEON 벡터화 Deep Dive — 인트린식, 자동 벡터화, simdjson, Highway, std::simd 완전 정복 (2025)
한국어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 SIMD | GPU SIMT |
|------|----------|----------|
| 모델 | 명시적 벡터 | 스칼라처럼 |
| 폭 | 4-16 | 32 (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);
}
}
`D`가 **ScalableTag** — 하드웨어에 따라 크기 결정. 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. 학습 리소스
**공식**:
- Intel Intrinsics Guide: https://www.intel.com/content/www/us/en/docs/intrinsics-guide/
- Agner Fog's manuals: https://www.agner.org/optimize/
**책**:
- "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: NEON → SVE / 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 SIMD │
│ AoSoA for cache + SIMD │
│ Column store │
│ │
│ 자동 벡터화: │
│ GCC/Clang: -O3 -march=native │
│ 확인: -fopt-info-vec │
│ 방해 요인: alias, branch, call, dep │
│ │
│ 포터블 SIMD: │
│ Google Highway │
│ C++26 std::simd │
│ Rust std::simd (nightly) │
│ WebAssembly SIMD (128-bit) │
│ │
│ 유명 활용: │
│ simdjson (4 GB/s JSON) │
│ AES-NI, SHA-NI, PCLMULQDQ │
│ Swiss 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. 퀴즈
**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 둘 다 친화.
**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 같은 포터블 추상화 사용. "컴파일러가 알아서 해줄 것"이라는 가정은 위험 — 한 줄의 수정이 조용히 벡터화를 제거할 수 있다.
**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배 성능"이 가능하다는 증명.
**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)이 이를 정리하려 하지만 아직 초기. 강력한 기술도 생태계/복잡도 문제로 제한될 수 있다는 교훈.
**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와 경쟁.
**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의 고전적 한계.
**A.** **포터블 SIMD 작성과 런타임 dispatch**. 전통적 접근은 `#ifdef __AVX2__ / __ARM_NEON__ / ...`로 여러 구현 + 컴파일 시 CPU 고정 → 유지보수 악몽, 한 바이너리가 다양한 CPU에 대응 불가. Highway: (1) **단일 코드** — `Add(va, vb)` 같은 추상화가 x86/ARM/WASM/SVE 모두에 동작. (2) **Scalable tag** — `ScalableTag<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/529)
- **SIMD** (Single Instruction, Multiple Data): 하나의 명령어로 여러 데이터를 병렬 처리. CPU의 숨은 N배 성능.