LabHub

블로그

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

TL;DR


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 속도 향상

이론적 최대:

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


2. 역사와 진화

2.1 MMX (1996)

Intel Pentium MMX. "MultiMedia eXtensions".

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

2.2 SSE (1999)

Streaming SIMD Extensions.

SSE1부터 SSE4까지 여러 세대:

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

2.3 AVX (2011)

Advanced Vector Extensions. Sandy Bridge부터.

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개 레지스터.

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

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

2.6 ARM NEON

ARM의 SIMD. ARMv7 (2004)부터.

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에서 다른 폭으로 동작.

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]

3.2 Horizontal ops

한 레지스터 내부:

[a0, a1, a2, a3, a4, a5, a6, a7] → a0+a1+a2+a3+a4+a5+a6+a7

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

3.3 Shuffle / Permute

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

[a0, a1, a2, a3] with mask [2, 0, 3, 1]
[a2, a0, a3, a1]

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

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]

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

3.5 Compare

두 벡터 비교 → mask:

[1, 5, 3, 7] > [4, 2, 6, 1][0, -1, 0, -1]  (-1 = all bits set)

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

3.6 Movemask

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

[0, -1, 0, -1, -1, 0, -1, 0]0b01011010

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

3.7 Gather / Scatter

비연속 메모리 접근:

gather(base, [3, 1, 7, 2]) = [base[3], base[1], base[7], base[2]]

느림 (여전히 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}

예:

데이터 타입:

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 자동

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


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

게임 엔진(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은 오픈소스. 많은 프로젝트 채택:

"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는 플랫폼 종속:

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 모두 지원.

사용처:


10. AVX-512 논쟁

10.1 강력하지만 복잡

AVX-512는 많은 것을 제공:

10.2 Subset 지옥

여러 subset이 있음:

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의 정리판":

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. 학습 리소스

공식:

:

블로그:

실습:

영상:


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. 학습 리소스 및 결론

공식 자료:

프로젝트:

관련 포스트:

댓글

아직 댓글이 없습니다.

로그인하면 댓글을 쓸 수 있습니다