네이티브 SIMD (vec<T, N>)
vec<T, N>는 일급 언어 타입입니다 — T 타입의 원소 N개로 이루어진
고정폭 벡터로, LLVM의 <N x T> 벡터 IR로 직접 낮춰집니다(lower). 기본적인
사용을 위해 따로 배워야 할 인트린식 기반 API는 없습니다 — 일반적인 산술
연산자와 첨자(subscript) 문법이 이미 그대로 동작합니다.
vec<float, 4> a = {1.0, 2.0, 3.0, 4.0};
vec<float, 4> b = {10.0, 20.0, 30.0, 40.0};
vec<float, 4> c = a + b; // 원소별 덧셈
vec<float, 4> d = c * b; // 원소별 곱셈
float first = c[0]; // extractelement — 레인 하나를 읽음
c[2] = 999.0; // insertelement — 레인 하나에 씀
c[0]은 (벡터의 원소 타입인) float입니다 — 이를 double에 대입하면
암묵적으로 확장(widen)되는데, 이는 다른 모든 수치 타입에 적용되는 것과
동일한 안전한 작은-타입-에서-큰-타입으로의 변환입니다(타입
참고): double first = c[0];는 캐스트 없이 컴파일되며 정확한 값을
그대로 읽어옵니다.
T는 어떤 정수 또는 부동소수점 기본 타입도 될 수 있고, N은 컴파일
타임 상수여야 합니다. "적당한" 하드웨어 폭으로 제한되지 않습니다 —
vec<int, 3>도 유효합니다 — 하지만 std::simd의
ISA별 편의 계층은 실제 벡터 레지스터에 깔끔하게 대응되는 폭을 선택합니다
(128비트 SSE/NEON/SIMD128, 256비트 AVX2, ...).
무엇이 무엇으로 컴파일되는가
컴파일러에는 아키텍처에 종속된 것이 아무것도 하드코딩되어 있지 않습니다 —
vec<T,N>는 이식 가능한 LLVM 벡터 IR로 낮춰지고, 각 타깃의 자체
백엔드가 스칼라 산술과 정확히 마찬가지로 거기서부터 명령어 선택을
수행합니다. 컴파일러가 지원하는 모든 백엔드에서 실제로 생성된 코드를
대상으로 검증되었습니다:
| 타깃 | vec<float,4> + vec<float,4>가 무엇이 되는가 |
|---|---|
| x86_64 (SSE2 기본) | addps (단일 명령어) |
| AArch64 / 32비트 ARM (NEON) | fadd v0.4s, v0.4s, v1.4s |
RISC-V (+v 확장) |
vsetivli + vfadd.vv |
| WebAssembly (SIMD128) | f32x4.add |
| SPIR-V | %v4float에 대한 OpFAdd |
ARM Cortex-M55/M85 (MVE, +mve) |
vadd.i32 q2, q0, q1 |
| CUDA (NVPTX) / ROCm (AMDGPU) | 스칼라 레인 연산 4개 — 아래 설명 참고 |
GPU 타깃은 스칼라화됩니다. PTX와 GCN에는 SSE/NEON처럼 "packed float
4개를 한 명령어로 더한다"는 개념이 없습니다 — GPU의 병렬성은 넓은 레지스터가
아니라 락스텝으로 실행되는 다수의 스레드(워프/웨이브프론트)에서
나옵니다. vec<float,4>는 그곳에서도 정확하게 동작하지만, 하나의 넓은
연산이 아니라 네 개의 독립적인 스칼라 연산으로 컴파일될 뿐입니다 —
모든 타깃에서 단일 명령어 처리량을 보장하는 것이 아니라, 관련된 값을
묶는 편리한 수단일 뿐입니다.
ARM Cortex-M4/M7의 DSP 확장은 별개의 이야기입니다. 이 확장의
packed-SIMD 명령어(SADD16, SMLAD, USAD8, ...)는 16비트 레인
2개 또는 8비트 레인 4개를 하나의 32비트 스칼라 레지스터에 채워
넣는데, 이는 실제 벡터 레지스터 파일과는 근본적으로 다른 모델이며
LLVM은 vec<T,N> IR을 이런 명령어로 자동 벡터화하지 않습니다. 이들은
std::dsp_* 함수로 별도로 노출됩니다 — 베어메탈
참고.
복합 초기화자
벡터 초기화자는 정확히 N개의 위치 기반 값만 받습니다 — 구조체/배열의
복합 초기화자와 달리 지정자(designator)는 허용되지 않습니다:
vec<int, 8> ints = {1, 2, 3, 4, 5, 6, 7, 8};
전역 벡터의 초기화자는 다른 전역 변수와 마찬가지로 여전히 컴파일 타임 상수여야 합니다:
vec<float, 4> global_v = {10.0, 20.0, 30.0, 40.0}; // 괜찮음 — 리터럴이 접혀서(fold) 상수가 됨
첨자 시맨틱
상수 인덱스로 v[i]를 읽는 것은 컴파일 타임에 경계 검사됩니다(범위를
벗어난 상수 인덱스는 컴파일 에러입니다). 런타임 인덱스는 unsafe { }
안이 아닌 한 다른 인덱싱 연산과 마찬가지로 경계 검사됩니다. 배열 원소와
달리 단일 레인은 메모리상에서 독립적으로 주소를 가질 수 없습니다 —
v[i]는 포인터 연산이 아니라, SSA로 존재하는 벡터 값 전체에 대한
extractelement/insertelement로 낮춰집니다.
포인터를 통한 로드와 스토어
vec<T, N> 포인터를 역참조하면 N개 레인을 한 번에 읽거나 씁니다:
unsafe {
*(vec<float, 8>*)&y[i] = *(vec<float, 8>*)&y[i] + a * *(const vec<float, 8>*)&x[i];
}
이런 포인터는 보통 배열 안 원소의 주소에서 오며, 원소 크기로만 정렬되어 있습니다. 그래서 이 접근은 벡터 자체의 정렬(32)이 아니라 원소의 정렬(여기서는 4바이트)을 가정하고, 정렬되지 않은 벡터 로드/스토어로 컴파일됩니다. 컴파일러가 직접 배치하는 벡터 지역 변수와 전역 변수는 전체 정렬을 유지합니다.
빌트인
| 빌트인 | 타입 | 낮춰지는 형태 |
|---|---|---|
__builtin_fmuladd(a, b, c) |
float, double, vec<float/double, N> (세 인자 모두 같은 타입) |
타깃에 FMA가 있으면 융합 곱셈-덧셈 하나, 없으면 곱셈과 덧셈 (llvm.fmuladd) |
__builtin_sqrt(x) |
float, double, vec<float/double, N> |
IEEE 제곱근 (llvm.sqrt); 하드웨어 sqrt가 있으면 명령어 하나이며, (sqrtf 호출과 달리) 루프 안에서 벡터화됩니다 |
일반 a * b + c는 반올림이 달라지므로 절대 FMA로 축약되지 않습니다. 내적이나 행렬 커널처럼 융합해도 되는 곳에서 __builtin_fmuladd를 씁니다.
ISA 매크로와 --target-cpu
컴파일러는 대상 CPU와 --target-features가 보장하는 기능에서 clang의 SIMD 기능 매크로를 정의합니다:
| 타깃 | 매크로 |
|---|---|
| x86 / x86-64 | __SSE2__, __SSE3__, __SSSE3__, __SSE4_1__, __SSE4_2__, __AVX__, __AVX2__, __FMA__, __F16C__, __AVX512F__, __AVX512BW__, __AVX512DQ__, __AVX512VL__ |
| AArch64 | __ARM_NEON, __ARM_FEATURE_SVE, __ARM_FEATURE_SVE2, __ARM_FEATURE_FP16_VECTOR_ARITHMETIC |
| 32비트 ARM | __ARM_NEON, __ARM_FEATURE_DSP(Cortex-M4/M7/M33), __ARM_FEATURE_MVE(Helium: 1 = 정수, 3 = 정수와 부동소수점) |
| WebAssembly | __wasm_simd128__ |
--target-cpu=native는 clang의 -march=native처럼 빌드 머신의 CPU와 그 모든 기능으로 빌드합니다. --target-cpu=znver4, --target-cpu=skylake-avx512 등으로 특정 CPU를 지정할 수도 있습니다. 둘 다 없으면 x86-64 코드는 베이스라인(SSE2)을 대상으로 하며, std의 핵심 커널은 대신 실행 시점에 ISA 수준을 고릅니다(CPU SIMD 수준 참고).
std::simd와의 관계
vec<T,N>는 컴파일러 수준의 빌딩 블록입니다. std::simd는
이 위에 완전히 구축된 이식 가능한 라이브러리로 — 타입 별칭(f32x4,
i32x8, ...), 벡터와 원시 포인터 간의 로드/스토어, 브로드캐스트,
수평(horizontal) 리덕션, 곱셈-덧셈 융합(FMA)을 제공하며, 여기에 더해
동일한 타입들을 아키텍처에 맞는 이름으로 다시 내보내기만 하는 얇은
ISA별 편의 헤더(x86_64.h, aarch64.h, riscv.h, wasm.h,
spirv.h, cortex_m.h, cuda.h, rocm.h)들이 있습니다. 타깃마다 별도로
손으로 작성된 SIMD 구현은 없습니다 — 전체 라이브러리는 백엔드마다 한 번씩
컴파일되는 동일한 이식 가능한 소스입니다.