✍️ 필사 모드: 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). 성능 향상의 경로:
- 멀티 코어 (병렬).
- ILP (Instruction-Level Parallelism, out-of-order 실행).
- SIMD (Data-Level Parallelism).
- 캐시 확장.
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의 파이프라인:
- Stage 1: SIMD로 structural mask 생성.
- Stage 2: Tape 만들기 (token 배열).
- 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. 퀴즈
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 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/550)
- **SIMD** (Single Instruction, Multiple Data): 하나의 명령어로 여러 데이터를 병렬 처리. CPU의 숨은 N배 성능.