최근 포토로그


Parallel Programming, OpenMP 그리고 Win32 - 5 복잡한컴퓨터이야기

최근 들어 부쩍 Parallel Computing에 대한 이야기가 많이 회자 되고 있는 이유는 물리적인 한계로 인해서 더 이상 CPU의 Frequency를 높이지 못한다는 데 그 원인이 있습니다. Frequency를 높이려면 전압이 높아지고 전압이 높아지면 발열이 높아지기 때문에 더 이상 Frequency를 높여서 성능을 향상시키기에는 한계가 명백하게 보이기 때문이기도 하지요. 최근에 나오는 multi core의 CPU들이 대략 3Ghz 전후의 clock speed를 가지고 있는데, 이는 최상의 Single Core 제품에 비해서 오히려 더 낮은 정도의 수준입니다. 성능의 개선 방식이 이제는 단일 CPU의 clock speed 개선에서, 그 괘를 이탈하여 Multi core로 이동하는 것은 어찌 보면 당연한 선택일지 모르겠습니다.

오늘은 master와 single에 대해서 알아보는 것으로 이야기를 시작해 볼까 합니다. 앞서 master thread라는 것이 Parallel Region이 시작되었을 때, 추가적인 thread를 생성하고, Parallel Region이 정상적으로 동작하는데 필요한 작업을 하는 thread라고 말씀 드린 바 있습니다. 이런 추가적인 작업을 수행하는 thread이니 만큼 OpenMP에서는 master thread는 약간의 특별 대우를 받습니다.(대빵 thread 입니다.) Parallel Region 내에서 master thread만이 호출할 수 있는 구간을 설정할 수 있다는 것인데요, 다음 코드를 먼저 살펴보시죠.

#pragma omp parallel num_threads(4)
{
_tprintf(_T("thread num : %d\n"), omp_get_thread_num());
#pragma omp master
{
_tprintf(_T("Master Thread : %d\n"), omp_get_thread_num());
}
}

만일 #pragma omp master 라는 directive가 없었다면, “Master Thread : %d”를 출력하는 문장도 4번 수행되었을 겁니다. 하지만 위 코드를 수행하면 결과는 다음과 같습니다.


image


“Master Thread”는 단 한번만 출력되었으며, 0번 thread에 의해서 호출되었음을 알 수 있습니다. 이와 비슷한 성격을 가진 single 이라는 construct도 있습니다. single은 master이던지 혹은 다른 thread이던지 상관없이 아무나 한번만 수행하면 되는 구간을 설정하기 위해 사용합니다. 아래 코드와 결과를 살펴보시죠. master thread가 single 이하를 수행하지 못하도록 하기 위해서 master thread를 0.1초간 재웠습니다.

#pragma omp parallel num_threads(4)
{
_tprintf(_T("thread num : %d\n"), omp_get_thread_num());
#pragma omp master
{
Sleep(100);
}

#pragma omp single
{
_tprintf(_T("single construct : %d\n"), omp_get_thread_num());
}
}

image


위 결과를 살펴보면 single construct 출력 루틴이 1번 thread에 의해서 단 한번만 호출되었음을 알 수 있습니다. 즉 master를 사용하면 “대장 나와”이고 single이라고 하면 “아무나 한 놈 나와” 이렇게 말하는 것과 같습니다. 그런데 이것 말고도 아주 중요한 차이점이 하나 있습니다. 아래 code 예제와 결과를 보면서 설명 드리는 게 좋겠습니다.

#pragma omp parallel num_threads(4)
{
#pragma omp master
{
Sleep(100);
}

#pragma omp single
{
_tprintf(_T("time: %d\n"), clock() - startTime);
_tprintf(_T("single construct : %d\n"), omp_get_thread_num());
Sleep(5000);
}

#pragma omp master
{
_tprintf(_T("time: %d\n"), clock() - startTime);
_tprintf(_T("master construct : %d\n"), omp_get_thread_num());
Sleep(5000);
}
#pragma omp single
{
_tprintf(_T("time: %d\n"), clock() - startTime);
_tprintf(_T("single construct : %d\n"), omp_get_thread_num());
}

_tprintf(_T("thread num : %d\n"), omp_get_thread_num());

}

image


제일 먼저 master thread를 0.1초간 재웠습니다. 이유는 바로 아래의 single construct로 지정한 문장을 master thread가 진입하지 않도록 하기 위함이지요. 그리고는 시간을 출력하고, thread 번호를 출력해 보았습니다. 위 결과에서는 2번 thread가 single로 지정한 루틴을 수행했습니다. 그런데 single로 진입한 thread는 5초간 멈춰있어야 합니다. 다른 thread들은 아무런 할 일이 없죠. 하지만 위 결과를 보면 알 수 있는 바와 같이 single로 진입한 thread가 해당 루틴을 완료할 때까지 다른 thread들은 single로 지정된 루틴이 완료될 때까지 대기하게 됩니다. 바로 single 바로 이하에 보이지 않는 implicit barrier가 있기 때문입니다. 그 덕에 master로 지정한 루틴으로 master thread가 진입할 때까지 약 5초가 걸렸습니다. master 루틴 내부에서도 약 5초간 대기를 수행합니다. 하지만 master thread만 대기하고 있고 다른 thread 들 중 하나(여기서는 2번)가 master 이하의 single로 진입해 버립니다. 이 점이 아주 큰 차이라고 할 수 있습니다. 정리하면 single construct로 지정된 문장에는 implicit barrier가 있으나 master로 지정된 문장에는 barrier가 없다고 정리하면 될 것 같습니다.


하지만 single로 지정한 문장 이하에 implicit barrer가 필요하지 않다면 다음과 같이 nowait를 사용하면 됩니다.


#pragma omp single nowait

위 예제의 single cluase에 nowait를 추가한 후 수행한 결과를 나타내어 보았습니다.


image


따라가기 조금 어렵겠지만, 그림을 그리면서 한번 따라가 보시기 바래요.


오늘은 몇 가지 더 알아보도록 하겠습니다. implicit barrer에 대해서는 계속 이야기를 했었는데요, 만일 명시적으로  barrier를 지정하시려면 다음과 같이 쓰시면 됩니다.

#pragma omp parallel num_threads(4)
{
#pragma omp master
{
MasterFunc();
}
#pragma omp barrier
Func();
}

master로 지정된 문장은 barrier가 없기 때문에 나머지 master thread가 MasterFunc()를 호출하는 동안 Func()를 먼저 호출할 수도 있지만, 위와 같이 명시적으로 barrier를 지정하게 되면 Func() 호출이전에 master thread가 MasterFunc()의 호출을 마칠 때까지 대기하게 됩니다. barrior는 마치 다음과 같이 win32로 표현할 수 있을 것 같습니다.

WaitForMultipleObjects(nCount, lpHandles, TRUE, INFINITE);

다음으로 알아 볼 것은 critical과 atomic 입니다. 이는 WIN32를 사용해 보셨다면 CRITICAL_SECTION과 InterLocked* 함수들과 비슷합니다. cirtical section을 지정하기 위해서는 다음과 같은 형태로 사용하게 됩니다.


#pragma omp critical([name])


name을 쓰지 않고 다수의 critical 을 사용하게 되면 모두 같은 critical section으로 판단하기 때문에 가능한 name은 지정하시는 게 좋겠습니다.

int sum1 = 0;
int sum2 = 0;
#pragma omp parallel for num_threads(4)
for (int i = 0 ; i < 20 ; i++)
{
#pragma omp critical(my)
sum1 += i;

#pragma omp critical(your)
{
sum2 -= i;
sum2 += i;
}
}

_tprintf(_T("sum1 : %d\n"), sum1);
_tprintf(_T("sum2 : %d\n"), sum2);

WIN32로 위와 같은 작업을 수행한다면 다음과 유사한 코드가 될 것 같습니다.

CRITICAL_SECTION my, your;

InitializeCriticalSection(&my);
InitializeCriticalSection(&your);

EnterCriticalSection(&my);
sum1 += i;
LeaveCriticalSection(&my);

EnterCriticalSection(&your);
sum2 -= i;
sum2 += i;
LeaveCriticalSection(&your);

DeleteCriticalSection(&my);
DeleteCriticalSection(&your);

다수의 문장을 포함해야 하는 critical section을 지정하는 것이 아니고, 수행해야 하는 작업이 위처럼 단순한 연산이라면 따로 critical을 사용하지 않고 atomic을 사용하셔도 됩니다.

int sum1 = 0;
int sum2 = 0;
#pragma omp parallel for num_threads(4)
for (int i = 0 ; i < 20 ; i++)
{
#pragma omp atomic
sum1 += i;

#pragma omp atomic
sum2 -= i;

#pragma omp atomic
sum2 += i;
}

_tprintf(_T("sum1 : %d\n"), sum1);
_tprintf(_T("sum2 : %d\n"), sum2);

물론 atomic 이하에 함수를 쓰거나 다수의 문장을 묶어 쓸 수는 없습니다. 이 부분을 WIN32로 표현한다면 아마 이렇게 될 것 같군요.

InterlockedExchangeAdd(&sum1, i);
InterlockedExchangeAdd(&sum2, -i);
InterlockedExchangeAdd(&sum2, i);

OpenMP가 내부적으로 Interlocked 함수를 쓰는지의 확실하지 않습니다. 단지 의미론적으로 가장 비슷한 함수를 가져왔을 뿐입니다.(이 부분은 나중에 확인해 보고 올리도록 하겠습니다.


눈에 보이기에는 critical을 사용하는 거나 atomic을 사용하는 것이 비슷해 보입니다만, critical에 비해서 atomic을 사용하는 것이 빠릅니다. INTEL CPU의 경우 위와 같은 atomic operation을 사용하면 Memory BUS에 대한 lock을 수행함과 동시에 덧샘을 수행하는 데, 이는 그냥 operand 하나 거든요. 하지만 critical section은 수천 개 이상의 명령을 수행해야 한답니다. 간단히 비교해 드릴께요. 기왕이면 앞서 배웠던 reduction까지 포함해서 3개를 비교해 보면 좋겠네요

clock_t startTime = clock();
long long sum = 0;
#pragma omp parallel for num_threads(4)
for (int i = 0 ; i < 10000000 ; i++)
{
#pragma omp critical(my)
sum += i;
}
_tprintf(_T("critical, sum : %I64d, time : %d\n"), sum, clock() - startTime);

sum = 0;
startTime = clock();
#pragma omp parallel for num_threads(4)
for (int i = 0 ; i < 10000000 ; i++)
{
#pragma omp atomic
sum += i;
}
_tprintf(_T("atomic, sum : %I64d, time : %d\n"), sum, clock() - startTime);

sum = 0;
startTime = clock();
#pragma omp parallel for num_threads(4) reduction(+:sum)
for (int i = 0 ; i < 10000000 ; i++)
{
sum += i;
}
_tprintf(_T("reduction, sum : %I64d, time : %d\n"), sum, clock() - startTime);

image


atomic이 critical에 비해서 거의 두 배 정보 빠른 것 같군요. 그런데 reduction의 결과 달랑 3!, 3!. 놀랍지 않으십니까?

Parallel Programming, OpenMP 그리고 Win32 - 1

Parallel Programming, OpenMP 그리고 Win32 - 2

Parallel Programming, OpenMP 그리고 Win32 - 3

Parallel Programming, OpenMP 그리고 Win32 - 4

Parallel Programming, OpenMP 그리고 Win32 - 5

Parallel Programming, OpenMP 그리고 Win32 - 6


덧글

  • bluenlive 2010/05/13 01:25 # 삭제 답글

    아... 멀티 코어 프로그래밍... MMX와 더불어... 언제쯤이나 이런데 도전해볼 수 있을까요?
  • 현호 2013/05/17 11:00 # 삭제 답글

    항상 좋은글 감사합니다.
댓글 입력 영역


facebook 프로필 위젯

트위터 위젯