한양과학기술고등학교

에듀테크 프로젝트 대시보드

SIMD 벡터 연산

SIMD 벡터 연산

Single Instruction, Multiple Data — CPU가 한 번의 명령으로 여러 값을 한꺼번에 계산하는 법

두 배열을 더한다고 해 보자. c[i] = a[i] + b[i] 를 원소마다 반복하면 원소가 1000개일 때 덧셈 명령도 1000번 실행된다. 이걸 스칼라(scalar) 방식이라 한다.

현대 CPU에는 폭이 넓은 벡터 레지스터가 있다. 예를 들어 256비트 레지스터에는 32비트 정수가 8개 들어간다. vaddps 같은 SIMD 명령 하나가 이 8쌍을 동시에 더한다. 명령 수가 1/8로 줄어드는 셈이다. "명령은 하나(Single Instruction), 데이터는 여럿(Multiple Data)" 이라서 SIMD다.

1. 스칼라 vs SIMD — 눈으로 비교

배열 A 와 B 를 더해 C 를 만든다. 두 방식이 같은 일을 어떻게 다르게 처리하는지 보자.

A
+
B
=
C
0실행한 덧셈 명령 수
–방식
–명령 수 절감

2. 레지스터 폭에 따라 레인 수가 달라진다

벡터 레지스터가 넓을수록 한 명령이 처리하는 데이터가 늘어난다. 슬라이더로 폭을 바꿔 보자. (데이터 타입: 32비트 float/int 기준)

128-bit (SSE)256-bit (AVX2)512-bit (AVX-512)1024-bit (가상)
256-bit 레지스터 한 개 = 8 lane
원소 1024개 덧셈에 필요한 SIMD 명령 수
스칼라 1024번 → SIMD 128번 (이론상 8× 적음)
명령어 집합레지스터 폭float32 lane등장
MMX64-bit–1997 (정수 전용)
SSE / SSE2128-bit41999~2001
AVX / AVX2256-bit82011 / 2013
AVX-512512-bit162016~
ARM NEON128-bit4스마트폰 대부분
ARM SVE가변 (128~2048)4~64슈퍼컴·서버

3. 코드에서는 어떻게 나올까

① 컴파일러 자동 벡터화 (auto-vectorization)

가장 흔한 경우. 그냥 평범한 루프를 짜면 컴파일러가 알아서 SIMD 명령으로 바꿔 준다.

// gcc -O3 -mavx2 로 컴파일하면 이 루프는 // 8개씩 묶어 vaddps 명령으로 자동 변환된다 void add(float* c, const float* a, const float* b, int n) { for (int i = 0; i < n; i++) c[i] = a[i] + b[i]; }

② 인트린식 (intrinsics) — 직접 SIMD 명령 지정

#include <immintrin.h> for (int i = 0; i < n; i += 8) { __m256 va = _mm256_loadu_ps(a + i); // 8개 로드 __m256 vb = _mm256_loadu_ps(b + i); __m256 vc = _mm256_add_ps(va, vb); // 8개 동시 덧셈 _mm256_storeu_ps(c + i, vc); // 8개 저장 }

③ 라이브러리 뒤에 숨어 있는 SIMD

# NumPy 한 줄. 내부적으로 SIMD 커널이 돈다 import numpy as np c = a + b # 파이썬 루프 없이 벡터화된 덧셈

NumPy·PyTorch·TensorFlow·이미지 코덱·게임 엔진·JSON 파서(simdjson)까지, 빠른 라이브러리 대부분이 SIMD를 쓴다.

4. 공짜가 아니다 — 벡터화의 걸림돌

꼬리 처리 (tail / remainder)

원소가 1000개인데 lane이 8개면 1000 = 8×125 로 딱 떨어진다. 하지만 1003개라면? 앞 1000개는 SIMD로, 남은 3개는 스칼라 루프로 따로 처리해야 한다.

메모리 정렬 (alignment)

일부 로드/스토어 명령은 데이터가 16·32바이트 경계에 맞아야 빠르다. _mm256_load_ps(정렬 필요) vs _mm256_loadu_ps(비정렬 허용, 살짝 느림).

분기 → 마스킹으로

레인마다 if 로 다른 길을 갈 수 없다. 대신 양쪽을 다 계산한 뒤 마스크로 골라 담는다.

// c[i] = (a[i] > 0) ? a[i]*2 : 0 을 벡터로 __m256 mask = _mm256_cmp_ps(va, zero, _CMP_GT_OQ); __m256 dbl = _mm256_mul_ps(va, two); __m256 vc = _mm256_and_ps(mask, dbl); // 조건 false인 레인은 0
데이터 의존성이 있으면(a[i] = a[i-1] + 1 처럼 앞 결과가 필요) 벡터화가 막힌다. 또 메모리 대역폭이 한계면 레인을 늘려도 더 안 빨라진다 — SIMD는 계산이 병목일 때 가장 효과가 크다.

5. SIMD ≠ 멀티스레드

SIMD멀티스레드
단위한 코어 안, 한 명령여러 코어
병렬 폭4~16 (레인)코어 수 (4~64)
비유한 사람이 8칸짜리 삽으로 한 번에 퍼냄8명이 각자 삽질
같이 쓰나?그렇다 — 스레드마다 SIMD 루프를 돌리고, GPU는 이 아이디어를 수천 배로 확장한 SIMT다

6. 정리

  • SIMD = 한 명령으로 여러 데이터를 동시에 — 레지스터 폭 ÷ 원소 크기 만큼의 레인
  • 대개 컴파일러 자동 벡터화나 NumPy 같은 라이브러리가 대신 해 준다
  • 이론상 최대 배속 = lane 수. 실제로는 꼬리·정렬·분기·메모리 대역폭이 발목을 잡는다
  • 멀티코어·GPU와 층위가 다른 병렬성이라 함께 쓴다