병렬 컴퓨터란 동시에 2개 이상의 프로세서가 동작하는 컴퓨터를 말합니다. 현재 대부분의 PC는 듀얼, 쿼드 코어 CPU를 사용합니다. 멀티 코어 CPU도 일종의 병렬 컴퓨터입니다. 지금부터는 병렬 컴퓨터에 대해 더 자세히 알아보도록 하겠습니다.
단일 프로세서/멀티 프로세서
병렬 컴퓨팅은 규모에 따라 크게 2가지로 나누어집니다.
- 소규모 멀티코어 환경: 대부분의 개인용 컴퓨터는 멀티 코어 CPU를 사용하고, CUDA, OpenMP 등의 소프트웨어 개발 환경이 지원되어 과거보다 훨씬 쉽게 병렬 프로그래밍이 가능합니다.
- 대규모 병렬 컴퓨팅 환경: 다수의 CPU나 컴퓨터 등을 연결하여 구현한 시스템으로, 클러스터(Cluster)나 그리드 컴퓨팅(Grid Computing) 등이 있습니다.
이전에는 하드웨어 속도의 발전에 따라 소프트웨어의 성능도 자동적으로 향상되었으나, 멀티코어 프로세서 상황에서는 이에 맞는 병렬 프로그래밍 개념이 필요하게 되었습니다. 따라서 단일한 CPU나 단일 코어 상황에서의 프로그래밍과 대조적으로 다중 CPU, 다중 코어의 상황을 고려해서 프로그래밍을 해야 현재 하드웨어 자원을 최대한 활용할 수 잇습니다.
위에서 말했듯, 다수의 CPU나 컴퓨터 등을 결합하여 구현된 시스템으로는 "클러스터"와 "그리드 컴퓨팅" 등이 잇습니다.
- 클러스터(Cluster): 근거리 네트워크(LAN)을 통해 연결된 컴퓨터들이 하나의 대형 멀티 프로세서로 동작하는 시스템
- 그리드 컴퓨팅(Grid Computing): 네트워크에 연결된 다수의 컴퓨터에 데이터를 전송하여 연산하게 한 후 이를 서버에서 취합하여 전체 연산을 수행하는 기술
병렬 컴퓨터 종류
프로세서들이 처리하는 명령어와 데이터 스트림의 수에 따라 병렬 컴퓨터의 종류를 분류할 수 있습니다.
- 명령어 스트림(Instruction Stream): 프로세서에 의해 실행되기 위하여 순서대로 나열된 명령어 코드들의 집합
- 데이터 스트림(Data Stream): 명령어들을 실행하는데 필요한 순서대로 나열된 데이터들의 집합
병렬 구조는 프로그램의 실행 속도를 증가시키기 위한 방법으로 여러 개의 명령어 스트림과 데이터 스트림을 처리하는 방식에 따라 아래와 같이 나눌 수 있습니다.
- SISD(Single Instruction Single Data): 한 번에 한 개씩의 명령어와 데이터를 순서대로 처리하는 단일 프로세서 시스템
- SIMD(Single Instruction Multiple Data): 모든 프로세서가 같은 프로그램을 수행하며, 각 프로세서가 병렬적으로 다른 데이터를 처리하는 구조이다. 여러 개의 데이터 스트림을 동시에 처리 가능합니다.
- MISD(Multiple Instruction Single Data): 다수의 프로세서들이 서로 다른 명령어들을 실행하지만, 처리하는 데이터 스트림은 한 개인 구조이다. 비현실적이기에 실제로 구현하지 않습니다.
- MIMD(Multiple Instruction Multiple Data): 각 프로세서가 서로 다른 프로그램을 수행하며 또 다른 데이터를 처리하는 구조입니다. 모든 프로그램들이 협력하여 통합적인 목적을 이루도록 하는 가장 복잡한 구조입니다.
병렬 프로그래밍
OpenMP를 통하여 병렬 프로그래밍을 진행해보도록 하겠습니다. OpenMP는 공유 메모리 기법의 멀티 프로세싱 프로그래밍 환경을 제공해주는 API로 쉽게 멀티 쓰레딩을 구현할 수 있도록 합니다. 쓰레드란 일련의 연속적으로 실행되는 인스트럭션을 말하고, 멀티 쓰레딩이란 마스터(master) 쓰레드가 다수의 슬레이브(slave) 쓰레드를 실행하여 병행 처리하는 방법을 말합니다.
다음은 "Hello World"를 출력하는 단일 쓰레드 프로그램입니다.
#include <stdio.h>
int main() {
printf("Hello World\n");
return 0;
}
다음은 "Hello World"를 출력하는 멀티 쓰레드 프로그램입니다. 원하는 개수만큼 쓰레드를 생성할 수 있고, 쓰레드를 생성한 개수만큼 문장을 출력합니다.
#include <stdio.h>
#include <omp.h> // OpenMP를 사용하기 위한 헤더 파일
int main() {
omp_set_num_threads(2); // 쓰레드를 int개 생성하라는 명령문
#pragma omp parallel // OpenMP의 지시자, 뒤따르는 블록을 병렬 처리하라는 명령문
{
printf("Hello World\n");
}
getchar();
return 0;
}
OpenMP를 이용하여 실제 예제를 구현해보겠습니다. 병렬 프로그래밍을 했을 때와 하지 않았을 때를 비교해보겠습니다.
#include <stdio.h>
#include <omp.h>
#include <time.h>
#define SIZE 10000
float data[SIZE][SIZE];
int main() {
int i, j;
float sum;
clock_t before;
double result;
// OpenMP를 사용했을 때
sum = 0.0;
before = clock();
#pragma omp parallel for
for (i = 0; i < SIZE; i++)
for (j = 0; j < SIZE; j++)
data[i][j] = i * j * clock();
#pragma omp parallel for
for (i = 0; i < SIZE; i++)
for (j = 0; j < SIZE; j++)
sum += data[i][j];
result = (double)(clock() - before) / CLOCKS_PER_SEC;
printf("with OpenMP : %7.5f\n", result);
// OpenMP를 사용하지 않았을 때
sum = 0.0;
before = clock();
for (i = 0; i < SIZE; i++)
for (j = 0; j < SIZE; j++)
data[i][j] = i * j * clock();
for (i = 0; i < SIZE; i++)
for (j = 0; j < SIZE; j++)
sum += data[i][j];
result = (double)(clock() - before) / CLOCKS_PER_SEC;
printf("without OpenMP : %7.5f\n", result);
getchar();
return 0;
}
컴파일시 OpenMP를 이용하여 병렬 프로그래밍을 했을 때 5.40200, 하지 않았을 때 9.96100으로 병렬 프로그래밍을 했을 때 속도가 2배 정도 빨라진 것을 확인할 수 있습니다.