최근 포토로그


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

OpenMP가 제공하는 일부 함수들은 library 형태로 제공되지만 compiler directive에 대한 해석과 코드 생성은 오로지 compiler의 몫입니다. 그렇다 보니 OpenMP를 사용할 수 있느냐 혹은 사용할 수 없느냐의 문제는 컴파일러가 OpenMP의 directive를 적절하게 처리할 수 있느냐 없느냐의 몫으로 귀결 됩니다. 단순히 library만을 제공하는 것으로 해결이 되지 않는다는 것이죠. 게다가 OpenMP의 가장 강력한 기능이라 할 수 있는 Parallel Loop 등의 기능과 C/C++의 STL이 잘 어울리지 않는 점도 눈에 띄는 단점일 수 있습니다. 그럼에도 불구하고 기존 code를 크게 수정하지 않고도(경우에 따라서) parallel processing을 할 수 있다는 것은 OpenMP의 장점이라고도 할 수 있겠습니다.

각설하고 오늘은 sections/section construct에 대해서 좀 살펴볼까 합니다. section construct는 Parallel region에서 병렬적으로 수행되어야 하는 statement를 구분하여 표기할 수 있는 방법인데요, 아래 예를 통해서 알아보기로 하죠.

#pragma omp parallel num_threads(4)
{
#pragma omp sections
{
#pragma omp section
{
_tprintf(_T("section 1,thread num : %d\n"), omp_get_thread_num());
}

#pragma omp section
{
_tprintf(_T("section 2,thread num : %d\n"), omp_get_thread_num());
}

#pragma omp section
{
_tprintf(_T("section 3,thread num : %d\n"), omp_get_thread_num());
}

#pragma omp section
{
_tprintf(_T("section 4,thread num : %d\n"), omp_get_thread_num());
}
}
}

위 예제 코드에서와 같이 parallel region내부에 sections를 구성하고 section 내부에 다수의 section을 나타낼 수 있습니다. sections 내부에 포함된 각 section들은 모두 동시에 수행될 수 있음을 나타냅니다. 따라서 상위 code의 4개 section은 모두 병렬적으로 수행될 수 있습니다. thread의 개수를 4개로 정의한 부분이 있음을 염두에 두시고 결과를 살펴보시죠


image



예상하실 수 있는 바와 같이 4개의 Thread가 생성되고 그 각각이 하나씩의 section을 수행하였습니다. 그렇다면 thread의 개수가 section의 개수보다 많다거나 혹은 section의 개수가 thread의 개수보다 많은 경우는 어떻게 될까요? 위 코드에서는 thread의 개수를 임의로 4개로 변경하였지만 thread의 개수는 이처럼 고정하지는 않는 것이 바람직 합니다. thread의 개수를 몇 개로 하는 것이 좋을 것인가에 대한 결정은 오로지 어떤 machine에서 이 프로그램이 수행될 것인가에 달려있는 것이니까요. 위 예는 동작 방식을 확인하기 위해서 임의로 그렇게 정했을 뿐입니다.


죽 프로그램이 어떤 machine에서 수행되느냐에 따라 thread의 개수는 가변적일 수 있으므로 각각의 section이 서로 다른 thread에 의해서 수행될 것임을 보장할 수는 없습니다. section을 구성하는 것은 단지 논리적으로 각각의 section들이 병렬적으로 수행될 수 있음을 나타내고 있을 뿐입니다.


thread의 개수를 2로 줄이고 test 해보면 다음과 같은 결과를 얻을 수 있습니다.


image


위 결과를 보면 0번 thread가 1,2,4번 section을 수행하였고, 1번 thread가 3번 section을 수행하였습니다. 하지만 이 내용은 얼마던지 바뀔 수 있습니다. 각각의 thread가 2개씩의 section을 수행할 수도 있고, 혹은 4개 모두의 section이 하나의 thread에 의해서 수행될 수도 있습니다.(가능성은 작겠지만요)


역으로 thread의 갯수를 좀 늘려 볼까요? 8정도로 늘린 후의 수행 결과입니다.


image



위 결과를 보면 3번 thread는 대신 4번 thread가 등장하였군요. 3번 thread는 놀고 있음에 분명합니다. 다시 말씀 드리지만 어떤 thread가 어떤 section을 수행할지는 보장할 수 없습니다. 단지 그것들이 병렬적으로 수행될 것임을 나타내는 것 뿐이지요. 이 부분은 크게 이해하기 어렵지 않으시리라 생각됩니다.


이것으로 중요한 compiler directive는 거의 살펴본 것 같습니다. 이제 관심을 조금 옮겨서 OpenMP가 제공하는 libarary function들에 대해서 좀 알아볼까 하는데요. 사실 함수의 개수가 그리 많지 않고 함수의 명칭만 보아도 금방 이해할 수 있기 때문에 쉽게 이해할 수 있으리라 생각합니다. library function은 크게 3가지 범주로 구분할 수 있는데 다음과 같습니다.


Execution Environment Function



  • omp_set_num_threads : parallel region에서 운용할 thread 개수를 지정 합니다.
  • omp_get_num_threads : parallel region내에서 생성된 thread 개수를 가져 옵니다.
  • omp_get_max_threads : 생성 가능한 최대 thread 개수를 지정 합니다.
  • omp_get_thread_num : thread별 고유번호를 가지고 옵니다.
  • omp_get_num_procs : 현재 application이 수행 중인 machine의 process 갯수를 가져 옵니다.
  • omp_in_parallel : parallel region 외부인지 아니면 내부인지 확인 합니다.
  • omp_set_dynamic : 뒤따르는 parallel region 내에서 thread의 개수를 가변적으로 변경할 수 있도록 합니다.
  • omp_get_dynamic : parallel region 내에서 thread의 개수가 동적으로 변경될 수 있는지의 여부를 확인 합니다.
  • omp_set_nested : nested paralleism을 가능하도록 할 지의 여부를 결정 합니다.
  • omp_get_nested : nested paralleism이 가능한지를 확인 합니다.

간단한 예제를 구성해 보았습니다. 제 machine의 quad core임을 감안하고 봐주시면 되겠습니다.

_tprintf(_T("omp_get_num_procs()=%d\n"), omp_get_num_procs()); // quad core(4)입니다.

omp_set_num_threads(2); // thrad의 갯수를 2개로
_tprintf(_T("omp_get_max_threads()=%d\n"), omp_get_max_threads()); // 당연히 2

_tprintf(_T("omp_in_parallel()=%d\n"), omp_in_parallel()); // prarallel region 밖이므로 0
_tprintf(_T("\n[parallel region]\n"));

#pragma omp parallel
{
_tprintf(_T("omp_get_thread_num()=%d\n"), omp_get_thread_num()); // 위에서 thread를 2개로 변경했으므로. 0,1
#pragma omp barrier
#pragma omp master
{
_tprintf(_T("\t[master]\n"));
_tprintf(_T("\tomp_in_parallel()=%d\n"), omp_in_parallel()); // parallel region 안이므로 1
_tprintf(_T("\tomp_get_num_threads()=%d\n"), omp_get_num_threads()); // thread 갯수는 2개
_tprintf(_T("\tomp_get_dynamic()=%d\n"), omp_get_dynamic()); // dynamic 설정은 default가 0
}
#pragma omp barrier
#pragma omp single
{
_tprintf(_T("\t[single]\n"));
_tprintf(_T("\tomp_get_thread_num()=%d\n"), omp_get_thread_num()); // thread 고유 번호는 1
}
}

image


dynamic 관련 함수와 nested 관련 함수는 조금 설명이 필요합니다. 위 결과에서 알 수 있는 바와 같이 아무런 설정이 없으면 omp_get_dynamic() 값은 0을 반환합니다. 이것은 default로 parallel region내에서 동적으로 thread의 갯수를 변경하지 못한다는 것이지요. 즉 parallel region이 시작될 때 생성된 thread의 개수가 해당 region을 빠져 나오는 순간까지 고정된다는 것이죠.  하지만 경우에 따라 parallel region 내에서 thread의 갯수가 동적으로 변경되는 것이 좀 더 효율적일 수도 있을 겁니다. 이 때 omp_set_dynamic()에 0 아닌 값을 전달하면, thread의 개수를 가변적으로 변경할 수 있도록 합니다. 물론 사용자가 필요할 때 manual하게 생성하는 것은 아니고 OpenMP가 알아서 판단합니다. omp_get_dynamic()은 dynmaic하게 thread의 갯수를 변경할 수 있는지의 여부를 반환합니다. omp_set_dynamic()의 signature를 살펴보면 parameter로 int형을 받도록 되어 있으나, 0으로 설정하면 thread의 개수를 가변적으로 변경할 수 있도록 하고, 그렇지 않으면 thread의 개수를 고정합니다.


omp_set_nested()는 nested parallel region 내에서도 parallel execution을 수행 할 지의 여부를 결정합니다. omp_get_nested()는 omp_set_nested()는 설정 값을 가져옵니다. nested parallel execution을 수행하지 않는 경우 0을 반환합니다. nested parallrel region에 대해서 익숙하지 않으실 테니 일단 코드로 어떻게 표현하는지부터 확인해 보죠

#pragma omp parallel num_threads(2)
{
_tprintf(_T("omp_get_thread_num()=%d\n"), omp_get_thread_num());

#pragma omp parallel
{
#pragma omp for
for
(int i = 0 ; i < 10 ; i++)
_tprintf(_T("i= %d, thread num = %d\n"), i, omp_get_thread_num());
}
}

즉 parallel 내에 또 다른 parallel이 또 포함되어 있는 경우, 포함된 parallel region을 nested parallel region이라고 합니다. 그런데 기본적으로는 nested된 region의 경우 parallel execution이 되지 않음을 아래 결과로 부터 알 수 있습니다.


image


두 개의 thread가 만들어졌지만, nested parallel region은 모두 0번 thread에 의해서 수행되고 있음을 알 수 있습니다. 만일 nested parallel region에 대해서도 parallel execution을 해야 한다면, omp_set_nested()를 이용하면 됩니다.


omp_set_nested(1); 추가하고 수행하면 다음과 같은 결과를 얻을 수 있습니다.


image


이 정도면 이해가 되셨으리라 생각합니다.


Lock Functions


lock 관련 함수들은 모두 thread간의 동기화를 수행할 목적으로 사용됩니다.



  • omp_init_lock : lock 개체를 초기화합니다.
  • omp_destroy_lock : lock 개체를 삭제합니다.
  • omp_set_lock : lock을 획득합니다. lock이 다른 thread에 의해서 사용되는 경우, 가용할 때까지 대기합니다.
  • omp_unset_lock : lock을 해제합니다.
  • omp_test_lock : lock을 획득하려고 시도합니다. lock을 성공적으로 획득한 경우 0 아닌 값을 그렇지 않은 경우 0을 반환합니다.

상위의 lock 관련 함수들은 nested parallel region에서는 사용되지 못합니다. 만일 nested parallel region에서도 사용 가능한 lock과 그에 대한 handling 함수들은 따로 정의되어 있습니다. 의미는 상위의 내용과 동일합니다. 조금 주의하셔야 하는 함수는 omp_test_lock() 인데요. 함수의 특성상 lock을 획득할 수도 있고, 획득하지 못할 수도 있습니다. lock을 성공적으로 획득한 경우 omp_unset_lock()을 반드시 호출해 주어야 하지만, lock을 획득하지 못한 경우에는 omp_unset_lock() 함수를 호출하지 말아야 합니다.



  • omp_init_nest_lock
  • omp_destroy_nest_lock
  • omp_set_nest_lock
  • omp_unset_nest_lcok
  • omp_test_nest_lcok

상위의 nest_lock 관련 함수들은 모두 omp_nest_lock_t 자료형을 취합니다. WIN32에 익숙하신 분들이라면 위 함수 목록을 보셨을 때, WIN32의 critical section 관련 함수들과 유사하다고 생각을 하셨을 겁니다.예제를 통해서 사용 예를 확인해 보시죠.

int sum1 = 0;
int sum2 = 0;
int lockCount = 0;

omp_lock_t simpleLock;
omp_init_lock(&simpleLock);

#pragma omp parallel num_threads(4)
{
#pragma omp for
for
(int i = 0 ; i < 10000 ; i++)
{
omp_set_lock(&simpleLock);
int temp = i;
sum1 += temp;
omp_unset_lock(&simpleLock);
}

#pragma omp for
for
(int i = 0 ; i < 10000 ; i++)
{
while (!omp_test_lock(&simpleLock))
{
#pragma omp atomic
lockCount++;
}

int temp = i;
sum2 += temp;
omp_unset_lock(&simpleLock);
}
}

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

omp_destroy_lock(&simpleLock);

딱히 설명할 내용이 없습니다. “#pragma omp critical 이랑 뭐가 다르냐?” 이렇게 물어보실 분도 있으시겠죠. 물론 위 코드만 보면 동일한 것 같습니다. 하지만 #pragma omp critical은 critical section의 시작~끝이 명확하게 정해져 버립니다. 하지만 이처럼 함수를 쓰게 되면, 조건에 따라서 lock을 하거나 하지 않는 등의 operation을 취한다거나, 특정 상황에서만 lock을 한다거나 하는 등의 복잡한 operation들을 구현할 수 있겠지요. 역시 compiler directive만으로는 표현력에 한계를 가질 수 밖에 없습니다.


Timing Routines


omp_get_wtime : 현재 시간 값을 가져옵니다. double 자료형의 값을 반환합니다. 단위는 초입니다.


omp_get_wtick : omp_get_wtime() 함수가 반환하는 값의 정밀도를 반환 합니다. double 자료형의 값을 반환합니다.


예제를 보면 간단합니다

double startTime = omp_get_wtime();

for (int i = 0 ; i < 10000000 ; i++)
;
double endTime = omp_get_wtime();
double precision = omp_get_wtick();

_tprintf(_T("start time = %f\nend time = %f\nprecision = %.10f\nelpase time = %f\n"),
startTime, endTime, precision, endTime - startTime);

별것 없죠?


이제 마지막으로 Environment variable에 대해서 알아보겠습니다.



  • OMP_SCHEDULE : 스케줄 방식과 chuck size를 결정합니다. “static”/ “dynamic”/ “guided” 중 하나
  • OMP_NUM_THREADS : thread의 갯수를 지정합니다. omp_set_num_threads(). 숫자 값으로 지정
  • OMP_DYNAMIC : thread의 개수를 가변적으로 변경할 수 있도록 합니다.omp_set_dynamic(), TRUE/ FALSE 로 지정
  • OMP_NESTED : nested paralleism을 가능하도록 할 지의 여부를 결정 합니다. omp_set_nested (), TRUE/ FALSE 로 지정

OMP_SCHEDULE은 schedule 이야기할 때 잠깐 언급 했었습니다. 나머지는 각각 대응되는 함수가 있어서 옆에 써 두었습니다. 함수에서의 설정과 환경변수에서의 설정이 서로 다른 경우가 있을 수도 있겠는데요, 이 경우 환경변수의 설정이 가장 후순위로 밀립니다.


몇 가지 예를 들어 보겠습니다.


SET OMP_SHCEULDE=”guided,4”
SET  OMP_NUM_THREADS=16
SET OMP_DYNAMIC=TRUE
SET OMP_NESTED=FALSE


OpenMP 2.0에 나와있는 내용은 거의 다 살펴본 것 같습니다. 2008년 5월에 OpenMP 3.0 spec.이 완성되었고, Task라는 훌륭한 기능이 새로 추가되었습니다만, 그에 대한 내용은 다루지 않으려고 합니다. (혹시 궁금하신 분이 계시면 참고자료에 OpenMP 3.0 spec.을 확인해 보시길 바래요.)


마치며…


Parallel Programming은 Hardware의 architecture에 따라(shared memory, distributed memory), Parallel의 종류에 따라(Bit-level, Instruction-level, data, Task)  그리고 Parallel Computer의 범주에 따라(Multicore, Symmetric, Distributed(Cluster, Massive parallel, Grid computing)) 다양한 적용 방법들이 존재하고, Parallel Programming 전용의 Language도 존재합니다. OpenMP는 그러한 방법 중 하나일 뿐이지만 Fortran이나 C/C++ 언어를 사용하는 개발자가 손쉽게 Parallel Programming을 할 수 있는 대표적인 방법임에는 분명한 것 같습니다.


기회가 된다면 Visual Studio 2010에 새로 추가된 Parallel Programming 관련 내용 (PPL, TPL 등)을 native와 managed로 나누어서 글을 써볼까 합니다만 언제가 될는지는 모르겠어요. :-)


OpenMP에 대해서 좀 더 자세히 알고 싶으신 분은 http://openmp.org/wp/resources 를 확인하시기 바랍니다.


감사합니다.

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/06/23 19:49 # 삭제 답글

    좋은 글 잘 읽었습니다.
    덕분에 OpenMP에 대해 개념도 잡고, 준비하고 있는 업무에도 잘 적용할 수 있을 것 같습니다.

    질문이 하나 있는데요, 멀티 코어 CPU에서 각 코어의 로드를 알 수 있는 방법이 있나요?
    API를 뒤져봐도 단일 코어에 대한 얘기밖에 없는 것 같아서요...
  • 다봉이 2010/08/21 22:31 # 삭제 답글

    정말 좋은 글이었습니다. for 병렬처리가 어떻게 이루어 지는지 몰랐는데 덕분에 잘 배우고 갑니다. 이제 수정을 시작해봐야겠습니다.
  • 김명신 2010/08/23 17:50 #

    홋~ 과찬의 말씀이십니다.
  • 설은 2010/12/29 08:45 # 삭제 답글

    좋은 가르침(?) 감사합니다^^ 제 블로그에 PDF파일로 만들어서 퍼갔습니당. 문제가 된다면 지우도록 하겠습니다^^;(출처와 저자는 남겼어요오~)
  • 김명신 2011/01/04 10:31 # 답글

    설은 : 널리 알려주셔서 감사합니다.
  • openmp 2011/01/14 14:07 # 삭제 답글

    좋은 정보 감사드립니다. ^^

    한가지 질문이 있는데요..
    OpenMP를 사용하면 주로 산술(?) 연산 처리 과정에서의 성능 향상뿐만 아니라, 다른 처리에서도 성능 향상을 기대할 수 있을까요?
    예를 들어, 소켓 통신에서 여러 메시지를 수신한 후 일련의 서브 처리하는 방식으로의 적용 등.
    어떤 작업 처리에서 OpenMP의 효과를 볼 수 있는지 실제 사례 등을 예로 설명해 주시면 좋겠습니다.
  • devilqoo 2012/05/18 18:45 # 삭제 답글

    좋은글 잘 보았습니다.
    오늘 하루 블로그 보면서 OpenMP 2.0에 대한 이해를 할 수 있었네요.
  • 5674 2014/09/16 10:04 # 삭제 답글

    잘 보고 갑니다. 그리고 몇개는 캡쳐해 갑니다. 감사합니다.
  • SONG 2015/11/03 06:01 # 삭제 답글

    openMP를 기본 내용들을 완전히 숙지할 수 있었습니다! 감사합니다!
  • 해적 2016/01/05 15:52 # 삭제 답글

    VS2010으로 개발하는데 OpenMP 2.0스펙문서보다가 설명이 부족한듯하여 뒤지다가 들어왔는데...
    총 6개 파트로 구성된 글들, 2~3번 정도 정독했습니다.
    설명 너무 잘하시구 예제도 너무 좋았습니다.
    많이 배우고 갑니다.
    참고로 PPL은 한빛에서 나온 e-book으로 보시면 간단하게 익힐 수 있을 듯 합니다.
    느낌은 작성자님의 글과 비슷합니다.
    감사합니다.
댓글 입력 영역


facebook 프로필 위젯

트위터 위젯