에듀테크 프로젝트 대시보드
SIMD 벡터 연산
두 배열을 더한다고 해 보자. c[i] = a[i] + b[i] 를 원소마다 반복하면
원소가 1000개일 때 덧셈 명령도 1000번 실행된다. 이걸 스칼라(scalar) 방식이라 한다.
현대 CPU에는 폭이 넓은 벡터 레지스터가 있다. 예를 들어 256비트 레지스터에는
32비트 정수가 8개 들어간다. vaddps 같은
SIMD 명령 하나가 이 8쌍을 동시에 더한다. 명령 수가 1/8로 줄어드는 셈이다.
"명령은 하나(Single Instruction), 데이터는 여럿(Multiple Data)" 이라서 SIMD다.
배열 A 와 B 를 더해 C 를 만든다. 두 방식이 같은 일을 어떻게 다르게 처리하는지 보자.
벡터 레지스터가 넓을수록 한 명령이 처리하는 데이터가 늘어난다. 슬라이더로 폭을 바꿔 보자. (데이터 타입: 32비트 float/int 기준)
| 명령어 집합 | 레지스터 폭 | float32 lane | 등장 |
|---|---|---|---|
| MMX | 64-bit | – | 1997 (정수 전용) |
| SSE / SSE2 | 128-bit | 4 | 1999~2001 |
| AVX / AVX2 | 256-bit | 8 | 2011 / 2013 |
| AVX-512 | 512-bit | 16 | 2016~ |
| ARM NEON | 128-bit | 4 | 스마트폰 대부분 |
| ARM SVE | 가변 (128~2048) | 4~64 | 슈퍼컴·서버 |
가장 흔한 경우. 그냥 평범한 루프를 짜면 컴파일러가 알아서 SIMD 명령으로 바꿔 준다.
NumPy·PyTorch·TensorFlow·이미지 코덱·게임 엔진·JSON 파서(simdjson)까지, 빠른 라이브러리 대부분이 SIMD를 쓴다.
원소가 1000개인데 lane이 8개면 1000 = 8×125 로 딱 떨어진다. 하지만 1003개라면? 앞 1000개는 SIMD로, 남은 3개는 스칼라 루프로 따로 처리해야 한다.
일부 로드/스토어 명령은 데이터가 16·32바이트 경계에 맞아야 빠르다. _mm256_load_ps(정렬 필요) vs _mm256_loadu_ps(비정렬 허용, 살짝 느림).
레인마다 if 로 다른 길을 갈 수 없다. 대신 양쪽을 다 계산한 뒤 마스크로 골라 담는다.
a[i] = a[i-1] + 1 처럼 앞 결과가 필요) 벡터화가 막힌다.
또 메모리 대역폭이 한계면 레인을 늘려도 더 안 빨라진다 — SIMD는 계산이 병목일 때 가장 효과가 크다.
| SIMD | 멀티스레드 | |
|---|---|---|
| 단위 | 한 코어 안, 한 명령 | 여러 코어 |
| 병렬 폭 | 4~16 (레인) | 코어 수 (4~64) |
| 비유 | 한 사람이 8칸짜리 삽으로 한 번에 퍼냄 | 8명이 각자 삽질 |
| 같이 쓰나? | 그렇다 — 스레드마다 SIMD 루프를 돌리고, GPU는 이 아이디어를 수천 배로 확장한 SIMT다 | |