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개 int32AVX (256비트): 8개 float, 4개 double, 32개 int8, 16개 int16, 8개 int32AVX-512(512비트): 16개 float, 8개 double, 64개 int82. 헤더 파일
섹션 제목: “2. 헤더 파일”#include <immintrin.h> // AVX, AVX2, AVX-512#include <emmintrin.h> // SSE2#include <smmintrin.h> // SSE4.13. 기본 명명 규칙
섹션 제목: “3. 기본 명명 규칙”_mm_<operation>_<type> // SSE (128비트)_mm256_<operation>_<type> // AVX (256비트)_mm512_<operation>_<type> // AVX-512 (512비트)
타입 접미사:ps = packed single (float)pd = packed doubleepi32 = 32비트 정수epi16 = 16비트 정수epi8 = 8비트 정수4. 벡터 덧셈 예제
섹션 제목: “4. 벡터 덧셈 예제”스칼라 버전
섹션 제목: “스칼라 버전”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]; }}SSE 버전 (4 float 동시 처리)
섹션 제목: “SSE 버전 (4 float 동시 처리)”#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];}AVX 버전 (8 float 동시 처리)
섹션 제목: “AVX 버전 (8 float 동시 처리)”#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];}5. 정렬된 메모리 로드/저장
섹션 제목: “5. 정렬된 메모리 로드/저장”정렬된 주소(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);6. 수평 합산 (Horizontal Sum)
섹션 제목: “6. 수평 합산 (Horizontal Sum)”벡터 레지스터의 모든 원소를 더하는 패턴입니다.
// __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);}7. 내적 (Dot Product)
섹션 제목: “7. 내적 (Dot Product)”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;}8. 이미지 처리: RGBA 채널 분리
섹션 제목: “8. 이미지 처리: RGBA 채널 분리”// 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]; }}9. CPU 기능 확인 (Runtime Detection)
섹션 제목: “9. CPU 기능 확인 (Runtime Detection)”#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); }}10. 컴파일러 플래그
섹션 제목: “10. 컴파일러 플래그”# GCC/Clangg++ -std=c++17 -O3 -mavx2 -mfma main.cpp
# MSVCcl /std:c++17 /O2 /arch:AVX2 main.cpp
# 자동 벡터화만 사용할 경우 (인트린식 없이)g++ -std=c++17 -O3 -ftree-vectorize -march=native main.cppSIMD 인트린식은 수치 연산 집약적 코드에서 극적인 성능 향상을 가져옵니다. 다만 코드 복잡도가 크게 증가하므로, 먼저 컴파일러 자동 벡터화(-O3 -march=native)로 충분한지 확인하고, 부족할 때만 수동 인트린식을 적용하는 것이 현명합니다. par_unseq 실행 정책이나 Eigen, ISPC 같은 라이브러리도 좋은 대안입니다.