플린 분류

플린 분류
Flynn's Taxonomy

단일 명령어 스트림
Single instruction stream
다중 명령어 스트림
Multiple instruction streams
단일 프로그램
Single program
다중 프로그램
Multiple programs
단일 데이터 스트림
Single data stream
SISDMISD
다중 데이터 스트림
Multiple data streams
SIMDMIMDSPMDMPMD

1 개요

스탠퍼드 대학의 교수인 마이클 J. 플린이 1966년에 제안한 컴퓨터 구조 분류이다.
명령어(Instruction)와 데이터 입력(Data stream)의 개수에 따라 구분한다.

2 분류

2.1 SISD

Single instruction stream, single data stream.
한 번에 데이터 하나를 명령어 하나로 처리하는 기법. 폰 노이만 구조의 컴퓨터는 기본적으로 이 기법을 따른다. 가장 기본적이고 간단한 구조이지만 명령어를 실행할 때마다 명령어와 데이터를 읽어와 처리해야 하기 때문에 효율이 떨어진다.

2.2 SIMD

Single instruction stream, multiple data streams.
한 번에 데이터 여러 개를 명령어 하나로 처리하는 기법.

현재 대부분의 프로그램은 SISD방식으로 동작한다. 즉 하나의 명령으로 하나의 데이터를 처리하는 것이고 2개의 데이터를 처리하려면 2번 연산한다. 라이브러리 수준의 프로그램이 아닌 경우에는 백이면 백 SISD로 구현되는데 CPU의 성능을 100% 활용하지 못하는 것이다. [1]

CPU 정보를 볼 수 있는(CPU-Z 같은) 프로그램에서 MMX, SSE, SSE2, SSE3 같은 걸 본 적이 있을 것이다. 이것이 바로 SIMD 기술인데 구체적으로는 해당 기술을 CPU에서 지원한다는 것이다. 최신 샌디브릿지 이상 CPU라면 AVX도 지원한다. 이 기술을 사용하려면 특수한 레지스터와 특수한 명령을 사용해야 하는데 최신예 컴파일러들은 Loop Unrolling 등의 기술을 통해 자체적으로 SIMD instruction을 사용한 코드로 변환해준다. 단, 코드를 컴파일러가 벡터 연산으로 인식하기 쉽게 작성해주어야 한다. 인텔의 icc, 마이크로소프트CL, GNU gcc, clang 등이 컴파일러 옵션을 통해 루프를 SIMD로 변환하는 것을 지원하며, icc의 경우엔 #pragma simd라는 특수한 전처리기를 지원한다.

Intel 진영과 달리, ARM 진영에서는 NEON이라는 SIMD 명령어 셋을 지원한다. Intel 진영과 비슷한 부분도 있지만, 아무래도 CPU 정책이나 구조가 다른 부분들도 많아서 주의하여 프로그래밍해야 한다.

icc의 경우에는 옵션을 통해 SIMD뿐만이 아니라 루프를 자체적으로 멀티쓰레딩으로 처리해주는 기능이 있으며, 최신 버전의 OpenMP에서는 SIMD 관련 전처리기를 추가하여 멀티쓰레딩과 SIMD의 두 마리 토끼를 동시에 잡으려는 시도를 하고 있다. 이런 기능을 사용하여 코드 최적화를 할 경우, 수치연산이 많은 코드는 컴파일러가 400% x 코어 개수 이상으로 성능을 뽑아내게 할 수도 있다. 컴파일러는 점점 똑똑해지고 프로그래머는 점점 멍청해진다. [2]

당연히 CPU의 종류에 따라 SIMD 명령어도 달라진다. 위에서 소개한 내용은 주로 Intel CPU들에 국한된 것이며, 예를 들어 모바일에 들어가는 ARM의 경우, NEON이라 불리는 다른 SIMD를 제공한다. 공통적으로 SIMD라는 기본적인 개념은 동일하지만 세세한 부분으로 들어가면 플랫폼에 크게 의존적이라 간주해도 좋을만큼 큰 차이를 보인다.

2.2.1 SIMD 레지스터

CPU에서 정보를 처리하기 위해서는 반드시 레지스터라고 하는 CPU내부의 임시 기억장소에 데이터를 적재해야 한다. 일반 범용 레지스터로 EAX, EBX, ECX, EDX 같은 게 있고 특수 용도 레지스터로 ESP, EBP 같은게 있는데 이런 것들이 모두 SISD용 레지스터다. SIMD용으로도 레지스터가 존재하며 여러 데이터를 한번에 처리하기 위해 일반 범용 레지스터보다 용량이 크다. XMM0, XMM1 같은 이름이 붙어있고 CPU가 지원하는 기술이 뭐냐에 따라 이 레지스터도 달라진다.

2.2.2 SIMD 명령어

위의 SIMD 레지스터에 저장된 데이터를 처리하는 어셈블리 명령어이다. 이것 하나만 실행하면 SIMD 레지스터에 저장된 데이터 전부가 처리돼서 또다른 SIMD 레지스터에 결과값이 저장된다. 예를 들어

 1. add eax, ebx
2. paddd xmm0, xmm1

위 두 명령은 eax += ebx, xmm0 += xmm1으로 서로 같아보인다.
하지만 xmm0 자체가 128비트 SIMD 레지스터이고 paddd 명령어는 128비트 SIMD 레지스터를 32비트 정수 네 개의 배열로 간주하고 연산하라는 뜻이다.[3] 2번 연산을 풀어서 쓰자면

 1. add xmm0[ 00.. 31], xmm1[ 00.. 31]
2. add xmm0[ 32.. 63], xmm1[ 32.. 63]
3. add xmm0[ 64.. 95], xmm1[ 64.. 95]
4. add xmm0[ 96..127], xmm1[ 96..127]

와 같이 네 번 반복하는 연산이다.[4] 즉 계산 속도를 이론적으로 4배 끌어올린다.[5]

AVX는 YMM 레지스터를 사용하며 연산 명령어도 다르다. YMM 레지스터는 256비트로 XMM의 두배. 즉 8배 빠르게 연산이 가능하다.

AVX-512는 AVX의 확장 명령어 세트이며, 제온 파이 나이츠 랜딩(72코어 288쓰레드의 CPU)과 스카이레이크 제온에 들어갈 예정인 SIMD 명령어 세트이다. 이름대로 512비트로, 이론상 AVX의 두 배의 속도로 연산을 가능하게 한다.

2.2.3 SIMD Intrinsics

위에서 소개한 어셈블리 명령어는 충분히 강력하지만 사용자의 입장에서는 그리 직관적이지는 않다. 게다가 관리하기도 어렵다. 그래서 보다 직관적인 방법으로 어셈블리 명령어를 사용할 수 있도록 도와주는 게 바로 Intrinsics다. C/C++ 계열이 메이저이면서도 하드웨어와 상대적으로 친숙한 언어이므로 Intrinsics는 주로 C나 C++ 형식을 따른다. 어셈블리 명령어를 직접 대입하는 것에 비하면 약간 느리고, 실제로 디어셈블리를 해보면 낭비되는 Instruction도 있지만, 생산성이나 유지, 보수의 측면에서는 훨씬 뛰어나기 때문에 성능 이슈가 그 미세한 차이가 유의미할 정도로 중요한 게 아니라면 일반적으로는 Intrinsics를 사용한다.

2.2.4 GPU에서의 SIMD

GPU의 영역에서는 SIMD가 당연시되고 있었다. 거기는 애초에 코어가 몇천 개씩 달려있는 다중프로세서 환경이라서 처음부터 SIMD형식으로 프로그램을 만들어왔다. 그 기능을 고스펙 게임과 슈퍼컴퓨터 등에서 범용적으로 사용하기 위해 개발된 기술이 GPGPU.

2.2.5 Web에서의 SIMD

마이너한 사실이긴 하지만 Web에서도 이 기법이 쓰인다. Dart가 대표적.

2.2.6 SIMD 의 한계

한번에 여러 개의 데이터를 묶어서 처리하는 개념이다보니 아무래도 프로그래머가 신경쓸게 많아진다. 예를 들어 데이터 개수가 4 혹은 8로 딱 나눠 떨어지지 않는 경우가 발생하지 않게 구조체에 패딩 바이트를 넣어서 원하는 벡터 크기에 맞춘다거나, malloc을 호출 할 때 벡터 크기의 배수만큼 메모리 할당을 하게 하는 기술을 익혀야 한다.

또한 코드를 작성할 때, 루프를 예쁘게 작성할 수록(CPU의 캐시 크기를 고려한다거나) 프로그램이 빨라지기에 관련된 지식(CPU 아키텍처 수준)을 빠삭하게 알아야 CPU의 진정한 성능을 끌어낼 수 있다는 것이 단점이다. CPU의 SIMD에 대비되는 GPGPU는 그런거 없이 코드를 작성해도 왠만해선 작성한 알고리즘의 극한까지 성능이 나오는 것에 비하면 확실한 단점. 대신 GPGPU를 남용하게 되면 엄청난 전력 손실에 시달리게 되는 것은 물론이고, 심지어 기대했던 성능 향상이 생각보다 이뤄지지 않을 수도 있으니 주의해야 한다. 예를 들어 모바일 환경에서 GPGPU에 크게 의존하게 되면 전력 소모로 인한 열 발생으로 인해, 기기에 쓰로틀링이 걸려 강제로 성능을 제약하는 모드로 진입하거나 강제 재부팅, 최악의 경우에는 기기 비산 현상까지 목격할 수 있다.

2.2.7 .Net Framework 지원

Microsoft가 시험판인 최신 JIT을 발표하면서 라이브러리 형태로 SIMD을 사용할 수 있도록 Nuget에서 Microsoft.Bcl.Simd을 배포하고 있다.

2.3 MISD

Multiple instruction streams, single data stream.
한 번에 데이터 한 개를 여러 명령어로 처리하는 기법. 예를 들어 곱셈을 하고 싶은데 명령어는 SHIFT 연산과 ADD 연산만이 있다고 하면, SHIFT와 ADD를 반복하여 곱셈을 구현할 수 있다. 이것을 소프트웨어 레벨이 아닌 프로세서 레벨로 구현한 것이 MISD이다. CPU에서 흔히 사용하는 파이프라인 기법이 이에 해당한다. CPU 밖에서 보자면 데이터 한 개를 넣어 결과 한 개를 얻어내므로 SISD와 크게 구별하지 않는 경향이 있다.

2.4 MIMD

Multiple instruction streams, multiple data streams.
한 번에 데이터 여러 개를 여러 명령어로 처리하는 기법. SIMD와의 차이점은 SIMD는 여러 데이터를 같은 인스트럭션으로 한꺼번에 처리하는 것이고 MIMD는 여러 데이터를 다른 인스트럭션으로 한꺼번에 처리하는 것이다.[6] 현대의 동시적 멀티스레드 프로그램들은 모두 MIMD라고 볼 수 있다. SPMD와 MPMD는 이 MIMD에서 더욱 세분화된 분류이다.
현 거의 모든 슈퍼컴퓨터는 이 MIMD를 기반으로 작동한다.

2.4.1 SPMD

Single program, multiple data streams.
다수의 프로세서가 같은 프로그램을 실행한다. 그래서 'Single processer, Multiple data'라고 부르는 경우도 있다.

2.4.2 MPMD

Multiple programs, multiple data streams.
다수의 프로세서가 최소 2개의 프로그램을 실행한다.

이것은 소니 플레이스테이션 3CELL 프로세서에서 적용된 방식이다.
  1. 극히 제한적이긴 하지만, 컴파일을 할 때 설정된 옵션에 따라 컴파일러가 자동적으로 특정 구문들은 SIMD로 번역하여 넣어주기도 한다.
  2. 정확히는, 같이 멍청해졌다는 표현이 맞다. 멀티쓰레딩이 등장한 이래로, 멀티 쓰레딩 특유의 이슈들에 주의를 기울이지 않고 작성된 코드를 컴파일러가 성능 향상을 위해 Reordering 등을 적용했을 때, 코드 작성자의 의도와 코드의 결과 값이 항상 맞는다는 보장이 없기 때문이다. 현 시점에서는 단일 쓰레드 기반의 코드에서만 보장된다. 괜히 숙련된 프로그래머들이 락을 걸거나 Atomic Type을 찾거나, 불변 변수에 집착을 하는 게 아니다.
  3. x86 레지스터 중 r로 시작하는 것들은 64비트, e로 시작하는 것들은 32비트이다.
  4. 32비트 이외에 16비트, 8비트 단위 연산도 가능.
  5. 물론 실제로는 매우 많은 변수가 있으므로 4배 끌어올린다는 보장이 없다. 예를 들어 명령 자체가 실제로는 실행 시간(클럭 수)이 더 걸리거나 데이터를 읽어 오는데 시간이 더 걸리는 등의 이유로 원래 명령 1개를 실행시키는 것에 비해 시간이 더 걸리는 경우도 있고, 현재의 운영체제는 멀티태스킹으로 여러 프로그램이 번갈아가며 작동하며 프로그램이 계산을 한번만 하고 끝나는 것이 아니므로 이전에 처리한 명령 및 데이터에 의해 CPU의 파이프라인, 캐시 등의 내용이 달라져서 실행 시간이 달라질 수도 있다. 또한 CPU나 칩셋, 메모리, 버스 등등의 구조 및 성능에 의해서도 영향을 받을 수 있다. 결론적으로 주어진 환경 및 개발자의 전략에 따라 천차만별의 결과가 나올 수 있다.
  6. 간단히 설명하자면 SIMD는 (10, 10, 10, 10)→(2, -4, 5, 3)→(4, 7, 2, 9)와 같은 데이터 입력 스트림에 (+), (*)와 같은 명령어 스트림이 있다면 연산 결과로 (12, 6, 15, 13)→(8, -1, 13, 4)가 나온다. 즉, 여러 데이터에 대해 모두 동일한 연산만을 적용할 수 있다. 반면 MIMD는 (10, 10, 10, 10)→(2, -4, 5, 3)→(4, 7, 2, 9)가 있으면 여기에 (+, -, *, %)→(-, +, %, *)를 적용할 수 있고 결과로 (12, 14, 50, 1)→(8, 13, 0, 9)을 얻을 수 있다. MIMD를 활용하면 병렬로 적용할 수 있는 연산을 모두 다르게 줄 수 있는 것이다. MIMD로 SIMD를 시뮬레이션할 수 있으나 그 역은 불가능하며 개념적으로는 SIMD를 MIMD의 특수한 한가지 형태로 간주할 수 있다.