CS

SIMD (Single Instruction Multiple Data) 란?

minkg3532 2026. 4. 30. 23:05

SIMD는 영어의 의미로 유추할 수 있듯이 하나의 명령어로 여러 개의 데이터를 동시에 처리하는 병렬 컴퓨팅 기술을 의미한다.

일반적인 CPU연산을 할 때 하나의 명령어로 하나의 연산 처리만 할 수 있다고 했을 때, SIMD를 이용해서 하나의 명령어로 여러 데이터를 한 번에 처리하여 효율성을 높일 수 있다.

 

전통적인 방식인 SISD(Single Instruction, Single Data)와 비교했을 때 아래와 같다.

  • SISD (일반 연산): $1 + 1$, $2 + 2$, $3 + 3$을 계산할 때, 더하기 명령을 세 번 내려서 순차적으로 처리한다.
  • SIMD (병렬 연산): $[1, 2, 3]$이라는 데이터 묶음과 $[1, 2, 3]$이라는 데이터 묶음을 준비한 뒤, "한 번의 더하기 명령"으로 $[2, 4, 6]$이라는 결과값을 한꺼번에 얻는다.

SIMD는 같은 연산을 여러 데이터에 동시에 적용할 때 쓰인다. 예를 들어 벡터 덧셈 A[i] + B[i]는 각 원소가 서로 독립적이고, 모든 원소에 똑같은 연산(예를 들어 덧셈)이 반복되는 구조이다. SIMD는 이 패턴을 하드웨어 수준에서 병렬로 처리한다. 반면 원소 간에 의존성이 있는 연산(예: 누적 합산처럼 앞 결과가 다음 계산에 필요한 경우)은 SIMD로 처리하기 까다롭다.

 

전개 과정은 다음과 같다.

1. 자동 벡터화(auto-vectorization)나, 직접 SIMD intrinsic 함수를 작성해서 SIMD 기능을 사용한다.

2. 데이터를 메모리에서 레지스터로 한 번에 가져와 로드한다. 이때 바이트 단위로 데이터 정렬이 되어 있을 때가 속도가 가장 빠르기 때문에, 이 점을 유의한다.

3. 벡터 연산을 실제적으로 실행한다. 즉, 특정 명령어(예: VADDPS)가 실행되면, 벡터 레지스터에 저장된 데이터를 명령어의 해석 규칙에 따라 여러 레인으로 나누어 읽는다. 각 레인의 데이터는 대응하는 PU로 전달되고, PU가 연산을 실행한 뒤 결과를 결과 레지스터의 같은 레인 위치에 저장한다.

 이 과정에서 레인은 레지스터의 비트를 해석하는 단위이고, PU는 그 데이터를 실제로 연산하는 회로이다. 레인 간에는 서로 의존성이 없으므로 각 PU는 독립적으로 작동하며, 순서도 없고 대기도 없다. 단, 모든 PU가 물리적으로 동시에 실행되는지는 하드웨어 구현에 따라 다르다. ( 256비트 연산을 한 번에 끝낼 수도 있고, 128비트 유닛을 써서 두 번에 나눠 처리(Double-pumping)할 수도 있다. - 아래에서 다시 언급함 )

 4. 해당 연산 결과를 메모리에 저장한다.

 

 

  • 레인 — 레지스터의 비트를 어떻게 쪼개 읽을지의 해석 규칙 (논리적 개념)
  • PU — 그 레인에서 읽은 데이터를 실제로 계산하는 회로 (물리적 개념)
  • 명령어 — 레인을 몇 개로 나눌지, 어떤 연산을 할지를 규정하는 규칙

 


대표적으로 SIMD 기법은 DirectX에서도 지원하고 있다. (벡터 연산)

여기서 유추할 수 있다 시피, SIMD는 SISD보다 그래픽 렌더링을 작업할 때 더 성능을 높게 하기 위해 사용될 수 있다.

 

그래서 데이터가 정량화되어 있고, 대량으로 처리해야 하는 작업에서 뛰어난 성능을 발휘할 수 있다.

다음은 해당 SIMD로 할 수 있는 주요 역할과 장점이다.

 

  • 연산 속도 향상: CPU 내의 특수 레지스터(예: AVX, NEON)를 활용하여 한 사이클에 여러 데이터를 처리하므로, 이론적으로 데이터 묶음의 크기만큼 속도가 빨라질 수 있다.
  • 멀티미디어 처리: 동영상 인코딩/디코딩, 오디오 처리 등은 수만 개의 픽셀이나 샘플에 동일한 필터링 효과를 적용해야 하므로 SIMD의 핵심 활용 분야이다.
  • 그래픽 및 물리 시뮬레이션: 3D 게임에서 수많은 정점(Vertex)의 좌표를 변환하거나, 물리 엔진에서 충돌 계산을 할 때 수치 연산 효율을 극대화할 수 있다.
  • AI 및 데이터 분석: 딥러닝의 핵심인 행렬 연산(Matrix Multiplication)은 대부분 SIMD 구조를 활용하여 가속화된다.

최신 CPU들은 이런 SIMD를 지원하기 위한 전용 명령어 집합(Instruction Set)을 가지고 있다.

 

  • x86, x64 (Intel/AMD): SSE(Streaming SIMD Extensions), AVX(Advanced Vector Extensions)
  • ARM: NEON

해당 SSE, AVX, AVX-512, NEON 모두 ISA(명령어 집합 규격)의 확장(Extension) 이다.

 

해당 그림 자료를 보다시피, ISA(명령어 집합 규격) 는 CPU가 이해할 수 있는 명령어의 전체 집합을 가리키는 큰 개념이다.

x86/x64이 그 ISA 자체이고, SSE·AVX·AVX-512는 그 안에서 "SIMD 연산"이라는 특정 기능을 추가로 정의한 확장 규격인 셈이다.

관계를 한 문장으로 표현하면, "SSE/AVX/AVX-512는 x86/x64 ISA의 SIMD 확장이다" 라고 말할 수 있다.

 

다시 돌아와서, 아래 이미지 자료를 한 번 살펴봐 보자

하나의 명령이 여러 데이터에 전파되는 과정

 

Instruction Stream을 통해 CPU의 명령어 디코더가 해석한 명령어(벡터 계산 등)를 의미한다. 일반적인 SISD 방식이라면 PU가 4개여도 데이터 4개를 처리하기 위해 명령어가 4개 필요하다. 하지만 SIMD는 명령어 하나로 여러 데이터를 한꺼번에 처리할 수 있다.

 이때 PU는 SIMD 유닛 안에서 실제로 덧셈, 곱셈 등의 산술 연산을 수행하는 물리적 연산 회로이고, Lane은 벡터 레지스터의 비트를 명령어의 규칙에 따라 나눈 논리적 데이터 단위라고 위에서 언급한 바 있다. 명령어가 실행되면 각 레인의 데이터가 대응하는 PU로 전달되어 독립적으로 연산된다. 여기서 SIMD 명령어는 하나의 원자적(Atomic) 작업으로 취급되기 때문에, 모든 PU의 연산 결과가 완벽히 준비되었을 때 결과 레지스터의 전체 비트(128/256/512비트)를 한꺼번에(Write-back) 업데이트한다. 일부 레인만 먼저 계산되어 레지스터에 기록되는 일은 없으며, 모든 레인의 연산이 끝나야 해당 명령어의 실행이 완료된 것으로 간주된다.

레인의 수는 결국 벡터 레지스터의 크기를 데이터 타입의 크기로 나눈 값이며, 이는 CPU가 지원하는 ISA(SSE/AVX/AVX-512)에 따라 결정된다.

 

레인 수는 "ISA와 레지스터 크기에 따라 레인 수가 결정된다"는 엄밀히 "x86/x64 ISA의 어떤 SIMD 확장을 쓰느냐(SSE/AVX/AVX-512)"에 따라 레지스터 크기가 달라지고, 그것이 레인 수를 결정한다는 뜻이다. 그리고 각 확장은 하위 호환을 보장한다. AVX-512를 지원하는 CPU는 AVX2와 SSE도 모두 실행할 수 있다.

아래 표를 참고해 보자

ISA 레지스터 크기 8비트 (byte) 16비트 (short) 32비트 (int/float) 64비트 (double)
SSE 128-bit 16
128÷8
8
128÷16
4
128÷32
2
128÷64
AVX / AVX2 256-bit 32
256÷8
16
256÷16
8
256÷32
4
256÷64
AVX-512 512-bit 64
512÷8
32
512÷16
16
512÷32
8
512÷64

 

다음 예를 보자, 만약 double( 64비트)연산을 총 5번 해야할 때, 아래와 같은 결과가 나오게 된다. 

ISA 레인 수 명령어 실행 횟수 낭비 레인 수
SSE 2 3번 1개
AVX2 4 2번 3개
AVX-512 8 1번 3개

128비트 크기의 SSE 레지스터(XMM)를 사용할 때, 64비트인 double 데이터 2개가 들어가면 레지스터의 모든 공간(비트)이 꽉 차게 된다. 그래서 동작 방식은 다음과 같이 이루어진다.

  1. 계산 직후: 2개의 double 합계가 레지스터에 기록된다. (이 시점에서 레지스터는 128비트가 다 찬 상태이다.)
  2. 메모리 저장: 레지스터는 다음 연산을 위해, 결과값을 영구히 보관해야 하므로 즉시 메모리(캐시/RAM)로 해당 값을 전송(Store)한다.
  3. 반복: 만약 더 계산해야 할 데이터가 있다면, 레지스터에 다음 double 2개를 다시 덮어씌워 채우면서 이 과정을 반복한다.

ISA 규격마다 해당 레지스터 개수가 다르다. 64비트(SSE/AVX)에 들어와서는 총 16개의 레지스터를 사용하고, AVX-512은 레지스터 개수가 32개로 가용할 수 있는 레지스터 횟수가 증가한다. 32비트 SSE는 8 개의 레지스터밖에 사용할 수 없었다.

 해당 레지스터의 연산 결과 데이터가 유실되는 것을 막기 위해서, 연산한 데이터들을 메모리로 올리고, 다음으로 들어오는 값들을 그 위로 덮어쓴다. 만약 하드웨어 아키텍처 규격에 따라 레지스터가 16개라면, 해당 16개의 데이터 결과값을 모아 놓고 메모리로 보낸다.

1. 벡터 레지스터( XMM, YMM, ZMM) )에 데이터가 존재함 ( PU에서 전달받은 연산 결과물 ) 레지스터는 연산이 끝나는대로(해당 연산 결과가 채워진 상태) CPU가 Store 명령을 내리면, 바로 대기 시간 없이 데이터를 스토어 버퍼로 보낸다. ( 레지스터가 16개라면, 16개의 Store 명령을 대기 )

2. Store Buffer에서 이를 모아, 메모리로 이동하는 버스로 데이터가 이동함 

3. L1, L2/L3 캐시를 거쳐 RAM(메인 메모리)에 적재됨

 

벡터 레지스터는 하위 호환성을 위해서, 물리적 저장공간은 하나인데, 어느 범위를 접근하느냐에 따라 부르는 이름이 달라진다. 새로운 저장공간이 생기는 게 아니라, 같은 공간의 일부에 다른 이름표를 붙인 것이다. CPU 입장에서 XMM, YMM, ZMM은 서로 다른 부품이 아니라, "내가 가진 이 거대한 비트 덩어리에서 어디까지를 연산 대상에 포함할 것인가?"를 결정하는 이름표인 것이다. 

이름 가리키는 구역 (Bit Range)
XMM0 0번 비트 ~ 127번 비트
YMM0 0번 비트 ~ 255번 비트
ZMM0 0번 비트 ~ 511번 비트

 

만약 레지스터를 하나의 YMM으로 지정시켜 만들어 버리면, 이전 CPU에서 지정한 XMM의 대한 명칭에 호환성이 안 맞게 되버리는 불상사가 일어난다. 이 점을 유의하여, 접근 범위 아래로 해당 XMM, YMM 명칭으로 맵핑하고, 데이터 버스를 공유하는 방식인 것이다.

 

다음 스케쥴러는 해당 명령어를 받은 뒤, 이를 아래에 있는 모든 Processing Unit(PU)에 동시에 신호 명령을 뿌려주는(Broadcasting) 역할을 한다. 해당 신호를 보내는 순간, Data Stream에 연산하기 위해 대기중인 데이터들이 PU로 들어가 계산을 실행하게 된다.

 

여기서 Data Stream( 주로 벡터 레지스터에서 PU로 데이터가 흘러가는 것 )에서 공급해준 각 레인 데이터들을 PU들이 동시에 계산을하고, 전부 계산이 끝난 값들을 다시 Data Stream 출력 경로를 통해서, 메모리나, 레지스터로 한번에 빠져나가게 된다.

  

여기서 PU는 ALU안에 존재하여, SIMD를 지원하는 CPU에서 ALU내부에 SIMD Unit안에 해당 PU들이 존재하고, 여기서 PU는 병렬로 배치되어 각 데이터들을 계산 처리를 하게 된다. 

 

여기서 해당 SIMD의 사용은, 게임 엔진이나 고성능 수치 계산 라이브러리에서는 프로그래머가 직접 Intrinsics 함수를 사용하거나, 컴파일러의 자동 벡터화(Auto-vectorization) 기능을 활성화하여 SIMD를 적용할 수 있다. 

 

레지스터 크기와 PU 연산 회로 너비의 불일치

레지스터는 256비트(YMM)인데 실제 연산 장치(PU/ALU)는 128비트인 경우가 실제로 존재한다. 이를 하드웨어 공학에서는 다음과 같이 처리한다.

마이크로 옵 분할 (Micro-op Splitting / Double Pumping)

  • 상황: CPU 설계 시 비용이나 전력 효율을 위해 256비트 전용 연산기를 만드는 대신, 기존의 128비트 연산기를 그대로 사용하는 경우
  • 동작: 256비트 AVX 명령어가 들어오면, CPU는 이를 128비트 연산 두 번으로 쪼개서 실행함.
    • 1사이클: YMM의 하위 128비트 연산
    • 2사이클: YMM의 상위 128비트 연산
  • 결과: 사용자(소프트웨어)는 256비트 레지스터를 쓰고 있다고 느끼지만, 물리적 하드웨어는 두 번에 나눠서 일하는 것이다. 여기서 만약 하위 128비트 영역을 담당하는 PU들이 연산을 마치면, 계산이 끝나자마자 하위 128비트 결과만 먼저 YMM 레지스터의 0~127 영역에 기록한다. ( 연산이 끝난 PU마다 레지스터로 바로바로 올린다. ) 

 

SIMD 활용 시 고려사항

  1. 데이터 정렬 (Data Alignment): 데이터가 메모리에 나란히 정열되어 있어야 효율적이다. 데이터가 흩어져 있으면 이를 모으는 데 시간이 더 걸릴 수 있다.
  2. 조건문 제약: 모든 데이터에 동일한 명령을 내려야 하므로, 데이터마다 다른 처리가 필요한 복잡한 분기문(if-else)이 많으면 SIMD의 효율이 급격히 떨어진다.