콘텐츠로 이동

SIMD Intrinsics 기초 가이드

SIMD는 하나의 명령어로 여러 데이터를 동시에 처리하는 CPU 기능입니다. x86 아키텍처에서는 SSE(128비트), AVX(256비트), AVX-512(512비트) 명령어 집합을 제공합니다. SIMD를 활용하면 루프 기반 수치 연산에서 4~16배의 성능 향상을 얻을 수 있습니다.


1. SIMD 레지스터 크기와 데이터 타입

섹션 제목: “1. SIMD 레지스터 크기와 데이터 타입”
SSE (128비트): 4개 float, 2개 double, 16개 int8, 8개 int16, 4개 int32
AVX (256비트): 8개 float, 4개 double, 32개 int8, 16개 int16, 8개 int32
AVX-512(512비트): 16개 float, 8개 double, 64개 int8

#include <immintrin.h> // AVX, AVX2, AVX-512
#include <emmintrin.h> // SSE2
#include <smmintrin.h> // SSE4.1

_mm_<operation>_<type> // SSE (128비트)
_mm256_<operation>_<type> // AVX (256비트)
_mm512_<operation>_<type> // AVX-512 (512비트)
타입 접미사:
ps = packed single (float)
pd = packed double
epi32 = 32비트 정수
epi16 = 16비트 정수
epi8 = 8비트 정수

void add_arrays_scalar(const float* a, const float* b, float* c, int n) {
for (int i = 0; i < n; ++i) {
c[i] = a[i] + b[i];
}
}
#include <emmintrin.h>
void add_arrays_sse(const float* a, const float* b, float* c, int n) {
int i = 0;
for (; i <= n - 4; i += 4) {
__m128 va = _mm_loadu_ps(a + i); // 비정렬 로드
__m128 vb = _mm_loadu_ps(b + i);
__m128 vc = _mm_add_ps(va, vb); // 4개 float 동시 덧셈
_mm_storeu_ps(c + i, vc); // 비정렬 저장
}
// 나머지 원소 처리
for (; i < n; ++i) c[i] = a[i] + b[i];
}
#include <immintrin.h>
void add_arrays_avx(const float* a, const float* b, float* c, int n) {
int i = 0;
for (; i <= n - 8; i += 8) {
__m256 va = _mm256_loadu_ps(a + i);
__m256 vb = _mm256_loadu_ps(b + i);
__m256 vc = _mm256_add_ps(va, vb);
_mm256_storeu_ps(c + i, vc);
}
for (; i < n; ++i) c[i] = a[i] + b[i];
}

정렬된 주소(16바이트/32바이트 경계)에서는 더 빠른 명령어를 사용할 수 있습니다.

// 정렬된 메모리 할당
alignas(32) float a[256]; // 32바이트 정렬
alignas(32) float b[256];
alignas(32) float c[256];
// 정렬된 로드/저장 (더 빠름, 하지만 정렬 보장 필수)
__m256 va = _mm256_load_ps(a + i); // aligned load
_mm256_store_ps(c + i, vc); // aligned store
// 동적 할당 시
float* buf = static_cast<float*>(
_mm_malloc(256 * sizeof(float), 32) // 32바이트 정렬
);
// 사용 후
_mm_free(buf);

벡터 레지스터의 모든 원소를 더하는 패턴입니다.

// __m128 (4 float) 수평 합산
float hsum_sse(__m128 v) {
__m128 shuf = _mm_shuffle_ps(v, v, _MM_SHUFFLE(2, 3, 0, 1));
__m128 sums = _mm_add_ps(v, shuf);
shuf = _mm_movehl_ps(shuf, sums);
sums = _mm_add_ss(sums, shuf);
return _mm_cvtss_f32(sums);
}
// __m256 (8 float) 수평 합산
float hsum_avx(__m256 v) {
__m128 lo = _mm256_castps256_ps128(v);
__m128 hi = _mm256_extractf128_ps(v, 1);
lo = _mm_add_ps(lo, hi);
return hsum_sse(lo);
}

float dot_product_avx(const float* a, const float* b, int n) {
__m256 sum = _mm256_setzero_ps();
int i = 0;
for (; i <= n - 8; i += 8) {
__m256 va = _mm256_loadu_ps(a + i);
__m256 vb = _mm256_loadu_ps(b + i);
// FMA: sum += a * b (한 번의 명령어로 곱셈+덧셈)
sum = _mm256_fmadd_ps(va, vb, sum);
}
float result = hsum_avx(sum);
// 나머지 처리
for (; i < n; ++i) result += a[i] * b[i];
return result;
}

// 32비트 RGBA 픽셀에서 R 채널만 추출
void extract_red_channel(
const uint8_t* rgba, // RGBA 인터리브 데이터
uint8_t* red, // R 채널 출력
int pixel_count
) {
int i = 0;
for (; i <= pixel_count - 8; i += 8) {
// 32픽셀(128바이트) 로드
__m256i pixels = _mm256_loadu_si256(
reinterpret_cast<const __m256i*>(rgba + i * 4)
);
// R 채널은 바이트 0, 4, 8, 12... 위치
// shuffle로 R 바이트만 추출
const __m256i shuffle_mask = _mm256_setr_epi8(
0, 4, 8, 12, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1,
0, 4, 8, 12, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1
);
__m256i r_bytes = _mm256_shuffle_epi8(pixels, shuffle_mask);
// 하위 8바이트에 R 값들이 모임
_mm_storel_epi64(
reinterpret_cast<__m128i*>(red + i),
_mm256_castsi256_si128(r_bytes)
);
}
// 나머지 처리
for (; i < pixel_count; ++i) {
red[i] = rgba[i * 4];
}
}

#include <cpuid.h> // GCC/Clang
// 또는 #include <intrin.h> // MSVC
bool supports_avx2() {
unsigned int eax, ebx, ecx, edx;
__cpuid_count(7, 0, eax, ebx, ecx, edx);
return (ebx >> 5) & 1; // AVX2 비트
}
bool supports_avx() {
unsigned int eax, ebx, ecx, edx;
__cpuid(1, eax, ebx, ecx, edx);
return (ecx >> 28) & 1; // AVX 비트
}
void process(const float* a, const float* b, float* c, int n) {
if (supports_avx2()) {
add_arrays_avx(a, b, c, n);
} else {
add_arrays_scalar(a, b, c, n);
}
}

Terminal window
# GCC/Clang
g++ -std=c++17 -O3 -mavx2 -mfma main.cpp
# MSVC
cl /std:c++17 /O2 /arch:AVX2 main.cpp
# 자동 벡터화만 사용할 경우 (인트린식 없이)
g++ -std=c++17 -O3 -ftree-vectorize -march=native main.cpp

SIMD 인트린식은 수치 연산 집약적 코드에서 극적인 성능 향상을 가져옵니다. 다만 코드 복잡도가 크게 증가하므로, 먼저 컴파일러 자동 벡터화(-O3 -march=native)로 충분한지 확인하고, 부족할 때만 수동 인트린식을 적용하는 것이 현명합니다. par_unseq 실행 정책이나 Eigen, ISPC 같은 라이브러리도 좋은 대안입니다.