병렬 계산의 분야에서는 "병렬 계산은 장래적으로는 각광을 받는다. 언제가 되어도 장래적으로" 등이라고 말 해지기도 합니다. 이것은 최근 수십 년에 있어 진실했습니다. 같은 일은 컴퓨터 아키텍쳐의 업계에도 있어 프로세서 클락의 고속화는 곧바로 한계에 이른다고 언제나 말해지고 있습니다만 실제로는 지금도 고속화가 계속 되고 있습니다. 멀티 코어 혁명은 이러한 병렬처리 분야에서의 낙관과 아키텍쳐 분야에서의 비관의 충돌이라고 말할 수 있습니다.
주요한 CPU 벤더는 클락 속도의 증가로부터 멀티 코어 프로세서에 의한 on-chip로의 병렬처리 지원의 제공으로 방향성을 바꾸고 있습니다. 생각은 단순하고 하나의 칩에 복수의 CPU 코어를 탑재하자고 하는 것입니다. 이것에 의해 하나개의 프로세서에 2개의 코어를 탑재하고, 시스템을 듀얼 프로세서 컴퓨터와 같이 동작시킬 수 있습니다. 게다가 하나의 프로세서에 4개의 코어를 탑재하면 시스템은 네 번째 프로세서와 같이 동작합니다. 이 방법에 의해 CPU 벤더는 고속화를 진행시키는 가운데의 기술적인 벽을 피하면서도 보다 퍼포먼스의 높은 프로세서를 제공할 수 있습니다.
여기까지는 훌륭한 일인 것 같이 들립니다만 어플리케이션이 멀티 코어를 활용하지 않으면 동작은 완전히 빨라지지 않습니다. 여기서 OpenMP의 등장입니다. OpenMP에 의해 C++ 개발자는 multi-thread 어플리케이션을 보다 재빠르게 작성할 수 있습니다.
OpenMP는 대규모이고 기능 풍부한 API 이기 때문에 이것에 대해 하나의 기사로 말하는 것은 매우 곤란합니다. 이 때문에 이 기사에서는 도입으로서OpenMP 의 여러 가지 기능을 사용해 multi-thread 어플리케이션을 재빠르게 작성하는 방법을 소개합니다. 상세한 것에 대하여는 OpenMP Web site (영어)에서 읽기 쉬운 사양이 입수 가능합니다.
Visual C++에서의 OpenMP의 유효화
OpenMP 표준은 1997년에 포터블한 multi-thread 어플리케이션을 작성하기 위한 API로서 정해졌습니다. 처음은 Fortran 베이스의 표준이었지만 뒤에 C 및 C++를 포함하도록 확장되었습니다. 현재의 버전은 OpenMP 2.0 입니다. Visual C++ 2005는 이 표준을 완전하게 지원 합니다. OpenMP는 또 Xbox 360 플랫폼에서도 지원 됩니다.
코드의 상세한 설명에 들어가기 전에 컴파일러로의 OpenMP 기능의 유효화에 대해 설명합니다. Visual C++ 2005에서는 컴파일러가 OpenMP 지시문을 해석할 수 있도록 하는 /openmp 컴파일러 스윗치가 새롭게 제공되고 있습니다.(또, 프로젝트의 프롭퍼티 페이지에서도 OpenMP 지시문을 유효하게 할 수 있습니다. [구성 프롭퍼티], [C/C++], [ 언어] 라는 것을 클릭하여 [OpenMP 서포트] 프롭퍼티를 변경합니다.) /openmp 스윗치가 호출 된 경우 컴파일러는 심볼 _OPENMP 를 정의합니다. 이것은 #ifndef _OPENMP를 사용해 OpenMP가 유효하게 되고 있는 것을 검출하는데 사용됩니다.
OpenMP는 임포트 라이브러리 vcomp.lib에 의해서 어플리케이션에 링크됩니다. 대응의 런타임 라이브러리는 vcomp.dll 입니다. 이 임포트 라이브러리 및 런타임 라이브러리의 디버그 버전(각각 vcompd.lib 과 vcompd.dll)에는 특정의 부정한 조작 때에 발생하는 추가의 에러 메세지가 있습니다. Visual C++은 OpenMP 런타임의 정적 링크는 지원 하고 있지 않는 것에 주의해 주세요. 다만 Xbox 360에서는 정적 링크가 지원 됩니다.
OpenMP로의 병렬처리
OpenMP 어플리케이션은 우선 하나의 스레드 즉 마스터 스레드로부터 작성합니다. 프로그램을 실행하면 어플리케이션은 마스터 스레드가 스레드 팀(마스터 스레드도 포함한다) 를 작성하는 병렬 영역에 들어갑니다. 병렬 영역이 끝나면 스렛드 팀은 정지 하고 마스터 스레드가 실행을 계속합니다. 하나의 병렬 영역에는 네이스트 된 복수의 병렬 영역이 존재할 수 있습니다. 네이스트 된 병렬 영역에서는 원래의 병렬 영역의 각 스레드가 스레드 팀의 마스터가 됩니다. 네이스트 된 병렬처리는 한층 더 다른 병렬 영역을 네이스트 할 수 있습니다.
<?xml:namespace prefix = v ns = "urn:schemas-microsoft-com:vml" />
그림 1 OpenMP 의 병렬 섹션
그림 1 에 OpenMP의 병렬처리의 동작을 나타냅니다. 가장 왼쪽의 황색 선이 마스터 스레드입니다. 이 스레드는 1의 포인트로 최초의 병렬 영역이 시작될 때까지는 싱글 스레드 어플리케이션으로서 실행됩니다. 병렬 영역에 들어가면 마스터 스레드가 스레드 팀(황색 및 오렌지색의 선으로 구성된다)을 작성합니다. 이러한 스레드는 모두 동시에 병렬 영역에서 실행됩니다.
2의 포인트에서는 병렬 영역에서 실행되고 있는 4개의 스레드 중 3개의 스레드가 새로운 스레드 팀(핑크색, 녹색, 청색)을 네이스트 된 병렬 영역에 작성합니다. 팀을 작성한 황색 및 오렌지의 스레드는 각각 자신의 팀의 마스터입니다. 각 스레드는 새로운 팀을 각각 다른 시점에서 작성할 수 있는 것에 주의해 주세요. 이것을 할 수 없으면 병렬 영역이 네이스트 될 것은 없습니다.
3 의 포인트에서 병렬 영역은 종료합니다. 네이스트 된 병렬 영역은 각각 그 영역 내의 스레드를 동기 합니다만 다른 영역과는 동기 되지 않는 것에 주의해 주세요. 4의 포인트에서는 최초의 병렬 영역이 종료하고 5의 포인트에서는 또 새로운 병렬 영역이 개시하고 있습니다. 5의 포인트가 새로운 병렬 영역에서는 각 스레드의 스레드 로컬 데이터는 앞의 병렬 영역으로부터 지속하고 있습니다.
이것으로 실행 모델의 기본적 이해를 얻을 수 있었다고 생각합니다. 여기에서 실제로 병렬 어플리케이션의 작성에 관한 설명에 들어갑니다.
OpenMP 의 구성
OpenMP는 사용하기 쉽고 단 2개의 기본적인 구성체, 프라그마 및 런타임 루틴으로 구성됩니다. OpenMP 프라그마는 통상 코드의 섹션을 병렬화하도록 컴파일러에 지시합니다. 모든 OpenMP 프라그마는 #pragma omp로 시작됩니다. 어느 프라그마의 경우에서도 이러한 지시문은 그 기능 여기에서는 OpenMP를 지원 하고 있지 않는 컴파일러에서는 무시됩니다.
OpenMP 런타임 루틴은 주로 환경에 관한 정보를 설정 및 취득하기 위해서 사용됩니다. 또 동기의 특정의 타입용의 API도 있습니다. OpenMP 런타임의 함수를 사용하려면 프로그램에서 OpenMP 헤더 파일 omp.h를 인클루드 할 필요가 있습니다. 어플리케이션이 프라그마만을 사용하는 경우는 omp.h를 생략 할 수 있습니다.
OpenMP 에 의해서 어플리케이션에 병렬처리를 추가하는 것은 간단하고 프라그마를 추가하고 필요한 경우에는 OpenMP 런타임으로부터 OpenMP 함수를 호출할 뿐입니다. 이러한 프라그마에는 다음의 형식이 사용됩니다.
#pragma omp <directive> [clause[ [,] clause]...]
지시문에는 parallel, for, parallel for, section, sections, single, master, critical, flush, ordered 및 atomic이 들어갑니다. 이러한 지시문은 워크 쉐어링 또는 동기의 구성체의 어느 쪽인가를 지정합니다. 이 기사에서는 이러한 지시문의 대부분에 임해서 설명합니다.
구는 지시문의 옵션의 수식자로 지시문의 동작에 영향을 줍니다. 지시문에는 다양한 구를 조합해 사용할 수 있습니다. 또 5개의 지시문(master, critical, flush, ordered 및 atomic) 은 구를 사용하지 않습니다.
병렬처리의 지정
많은 지시문이 있습니다만 조금씩 시작합시다. 가장 자주 사용되고 한편 중요한 지시문은 parallel 입니다. 이 지시문은 디렉티브의 뒤에 계속 되는 구조화 블록의 동적 범위를 나타내는 병렬 영역을 작성합니다. 예를 들어 다음과 같이 됩니다.
#pragma omp parallel [clause[ [, ]clause] ...]
structured-block
이 지시문은, 구조화 블록이 복수의 스레드로 병렬에 처리되는 것을 컴파일러에게 전합니다. 각 스레드는 같은 명령 스트림을 실행합니다만 명령의 같은 세트일 필요는 없습니다. 이것은 if-else 등의 제어 플로우 스테이트먼트에 의해서 정해집니다.
여기서 친숙한 "Hello World" 프로그램의 샘플을 나타냅니다.
#pragma omp parallel
{
printf("Hello World\n");
}
2개의 프로세서의 경우 다음과 같은 출력이 되는 것을 기대하겠지요.
Hello World
Hello World
그러나 다음과 같이 될 가능성도 있습니다.
HellHell oo WorWlodrl
d
2번째의 출력은 2개의 스레드가 병렬로 실행되어 그 양쪽 모두가 동시에 출력하려고 한 결과입니다. 복수의 스레드가 하나의 공유 자원(이 경우의 공유 자원은 콘솔 윈도우) 를 읽어 들이거나 변경하거나 하는 경우 경합 상태가 발생할 가능성이 있습니다. 이것은 어플리케이션 중에서는 비결정적인 버그가 되어 찾아내는 것이 곤란하게 되는 경우도 있습니다. 프로그래머는 이러한 버그가 발생하지 않게 할 필요가 있습니다. 통상은 락을 사용하던지 또는 가능한 한 공유 자원을 피합니다.
다음으로 좀 더 도움이 되는 샘플을 소개합니다. 하나의 배열에 포함된 2개의 값의 평균을 계산해 그 값을 다른 배열에 세트 합니다. 여기에서는 새로운 OpenMP의 구성체 #pragma omp for를 사용합니다. 이것은 워크 쉐어링 지시문입니다. 워크 쉐어링 지시문은 병렬처리를 작성하는 것이 아니라 스레드 팀을 논리적으로 분배해 뒤에서 계속 되는 제어 플로우의 구성체를 구현합니다. #pragma omp for 워크 쉐어링 지시문은 이하의 코드에 나타내는 for 루프가 병렬 영역으로부터 불려 갔을 경우 반복을 스레드 팀에서 분할하는 것을 OpenMP에게 전합니다.
#pragma omp parallel
{
#pragma omp for
for(int i = 1; i < size; ++i)
x[i] = (y[i-1] + y[i+1])/2;
}
이 케이스로 size의 값을 100으로 하고 4 개의 프로세서를 탑재하는 컴퓨터로 실행하면 루프의 반복은 프로세서 1에 반복의 1 ~ 25, 프로세서 2에 반복의 26 ~ 50, 프로세서 3에 반복의 51 ~ 75, 그리고 프로세서 4에 반복의 76 ~ 99이 각각 할당할 수 있습니다. 이것은 스케줄링 폴리시가 static 스케줄링인 것을 전제로 합니다. 스케줄링 폴리시에 대해서는 이 기사의 다음에 상세하게 설명합니다.
이 프로그램에서는 명시적이 아닙니다만 병렬 영역의 마지막에는 바리어 동기가 있습니다. 모든 스레드는 마지막 스레드가 완료할 때까지 여기서 블록 됩니다.
이하와 같이 앞에 나타낸 코드의 #pragma omp for를 사용하지 않으면 스레드가 각각 for 루프 전체를 실행하므로 각 스레드의 처리는 장황하게 됩니다.
#pragma omp parallel //
아마 의도적인 것은 아니다
{
for(int i = 1; i < size; ++i)
x[i] = (y[i-1] + y[i+1])/2;
}
병렬 루프는 가장 자주 사용되는 병렬화 가능한 워크 쉐어링의 구성체이므로 OpenMP에는 #pragma omp parallel의 뒤에 #pragma omp for를 기술하는 생략 형식이 제공되고 있습니다. 다음과 같이 됩니다.
#pragma omp parallel for
for(int i = 1; i < size; ++i)
x[i] = (y[i-1] + y[i+1])/2;
여기서 루프 반송 의존이 없는 것에 주의해 주세요. 즉, 루프 1개의 반복이 그 루프의 다른 반복에 의존하고 있지 않습니다. 예를 들어 다음의 2개의 루프에는 루프 반송 의존의 특성이 2개 있습니다.
for(int i = 1; i <= n; ++i) //
루프 (1)
a[i] = a[i-1] + b[i];
for(int i = 0; i < n; ++i) //
루프 (2)
x[i] = x[i+1] + b[i];
루프 1을 병렬처리 하는 데는 문제가 있습니다. 루프 i 의 반복을 실행하기 위해서 i-1의 결과를 취득할 필요가 있기 때문에입니다. i의 반복은 i-1의 반복 해에 의존합니다. 루프 2를 병렬처리 하는 것도 문제입니다. 정확히 이유는 다릅니다. 이 루프에서는 x[i-1]의 값을 계산하기 전에 x[i] 를 계산할 수 있습니다. 그러나 x[i] 를 계산하면 x[i-1]의 값이 바뀌어 버립니다. i-1의 반복은 i의 반복 해에 의존합니다.
루프를 병렬처리 하는 경우는 루프의 반복에 의존관계(dependencies)가 존재하지 않게 합니다. 루프의 반복에 의존이 없는 경우 컴파일러는 그 루프를 어떠한 순서에서도 즉 병렬에 실행할 수 있습니다. 이것은 컴파일러에서는 체크되지 않는 중요한 요건입니다. 사실상 개발자가 병렬처리 되는 루프에는 루프 반송 의존이 포함되지 않는 것을 컴파일러에 단언할 수 밖에 없습니다. 루프가 의존관계(dependencies)를 가지는 경우에 컴파일러에 병렬처리를 하도록 설정하면 컴파일러는 설정된 대로 실행해 최종적으로 잘못된 결과가 되어 버립니다.
이외에도 OpenMP에서는 #pragma omp for 또는 #pragma omp parallel for의 블록 내에서 가능한 for 루프의 형식에 제한이 있습니다. 루프는 다음의 형식일 필요가 있습니다.
for([integer type] i = loop invariant value;
i {<,>,=,<=,>=} loop invariant value;
i {+,-}= loop invariant value)
이것은 OpenMP가 루프의 최초에서 루프가 실행하는 반복의 회수를 판단하기 위해서 필수입니다.
OpenMP 와 Win32 스레드의 비교
앞에 나타낸 #pragma omp parallel for의 샘플과Windows API를 사용해 스레드화 하기 위해 필요한 일을 비교해 보고 알 수 있는 것이 몇 개인가 있습니다. 그림 2 로부터 같은 결과를 얻기 위해서보다 많은 코드가 필요하다라고 하는 것을 압니다. 또, 여기에는 수면 아래에서 행해지는 트릭과 같은 것도 존재합니다. 예를 들어 ThreadData의 constructor는 스레드 호출할 것에 개시와 종료를 계산합니다. OpenMP는 이러한 상세를 자동적으로 처리합니다. 게다가 프로그래머가 병렬 영역 및 코드의 구성을 조정하는 것도 가능하게 합니다.
공유 데이터와 프라이빗 데이터
병렬 프로그램을 작성할 때는 퍼포먼스의 향상을 위해서 뿐만이 아니고 정상적인 처리를 하듯이 하기 위해서 어느 데이터가 공유되어 어느 데이터가 프라이빗이 되는지를 이해해 두는 것이 매우 중요합니다. OpenMP는 이 구별을 프로그래머에게는 확실하게 나타내 보입니다. 또 수동으로 조작할 수도 있습니다.
공유 변수는 스레드 팀의 모든 스레드로 공유됩니다. 즉 1개의 스레드로 공유 변수로 변경을 더하면, 그 변경은 그 병렬 영역 내의 다른 스레드로부터 인식됩니다. 한편 프라이빗 변수는 스렛트 팀의 스레드 마다 프라이빗으로 작성됩니다. 이 때문에 1개의 스레드로 변경을 더해도 그 변경은 다른 스레드의 프라이빗 변수에서는 인식되지 않습니다.
기정에서 병렬 영역 내의 모든 변수는 공유입니다. 다만 예외가 3개 있습니다. 첫번째는 parallel for 루프에 있어서의 루프의 인덱스로 이것은 프라이빗입니다. 그림 3의 샘플로 i 변수는 프라이빗입니다. j 변수는 기정에서는 프라이빗이 아닙니다만 firstprivate 구를 사용해 명시적으로 프라이빗으로 되고 있습니다.
2번째로서 병렬 영역의 블록에 로컬인 변수는 프라이빗이 됩니다. 그림 3의 변수 doubleI은 병렬 영역에서 선언되고 있기 때문에 프라이빗입니다. myMatrix::GetElement으로 선언된 비정적 변수 및 비 멤버 변수는 모두 프라이빗이 됩니다.
3번째의 예외는 private, firstprivate, lastprivate 또는 reduction 구에 리스트 된 변수로 이것들은 모두 프라이빗이 됩니다. 그림 3 의 변수 I, j 및 sum은 스레드 팀의 각 스레드로 프라이빗이 됩니다. 이것은 이러한 변수의 카피를 스레드 마다 따로 따로 작성하는 것으로써 실현됩니다.
4개의 구는 모두 변수의 리스트를 받습니다만 그러한 시멘틱스는 모두 다릅니다. private 구는 리스트내의 각 변수의 사적인 카피가 스레드 마다 작성되는 것을 지정합니다. 이 프라이빗 카피는 기정치로 초기화됩니다(적절한 경우에는 기정의 constructor가 사용됩니다) . 예를 들어 int 형태의 변수의 기정치는 0 입니다.
firstprivate 구는 시멘틱스는 private 와)과 거의 같습니다만, 병렬 영역이 각 스레드에 들어가기 전에 프라이빗 변수의 값을 카피합니다. 적절한 경우에는 카피 constructor가 사용됩니다.
lastprivate 구는 시멘틱스는 private와 거의 같습니다만 마지막 반복해 또는 워크 쉐어링의 구성체의 마지막 섹션으로 lastprivate 구에 리스트 된 변수의 값이 마스터 스레드의 변수에 할당할 수 있습니다. 적절한 경우에는 오브젝트를 카피하는데 카피 할당 연산자가 사용됩니다.
reduction 구는 private의 시멘틱스를 닮아 있습니다. 다만 변수와 연산자의 양쪽 모두를 받습니다. 연산자의 편성은 그림 4 에 일람 한 연산자에 한정됩니다. reduction 변수는 스칼라 변수(예를 들어 float, int 또는 long 등 이며, std::vector, int [] 등에서는 없는 변수) 일 필요가 있습니다. reduction 변수는 스레드 마다 겉(표)에 일람 한 값에 초기화됩니다. 코드 블록의 최후로 reduction 연산자가, 변수의 각 프라이빗 카피에 적용되어 한층 더 변수의 원래의 값에도 적용됩니다.
그림 3의 샘플에서는 sum의 값은 각 스레드로 암묵적으로 0.0f으로 초기화됩니다. (겉(표)의 정규화 값은 0인 것에 주의해 주세요. 이것은 sum의 형태가 float 이기 위해서 0.0f 이 됩니다.) #pragma omp for 블록이 완료한 후 스레드는 + 연산자를 모든 프라이빗 sum의 값, 및 원래의 값(sum의 원래의 값, 이 샘플에서는 10.0f)에 적용합니다. 결과는 원의 공유 sum 변수에 할당할 수 있습니다.
루프 이외의 병렬처리
OpenMP는 루프 레벨로의 병렬처리에 자주 사용됩니다만 함수 레벨로의 병렬처리도 지원하고 있습니다. 이 메커니즘은 OpenMP sections으로 불려 갑니다. sections의 구조는 간단하고 많은 경우에 유용합니다.
컴퓨터 과학에 있어 가장 중요한 알고리즘인 퀵 소트에 대해서 생각해 봅시다. 여기서 채택하는 예는 일련의 정수를 대상으로 하는 단순한 재귀적 퀵 소트 알고리즘입니다. 단순하게 하기 위해 일반인 형식의 버전은 사용하지 않고 OpenMP의 생각을 채용합니다. 그림 5의 코드는 sections을 사용하는 퀵 소트의 메인이 되는 함수입니다(여기에서는 간단하게 하기 위해서 Partition 함수에 대해서는 생략 합니다) .
이 샘플에서는 최초의 #pragma에서 섹션의 병렬 영역을 작성합니다. 지시문 #pragma omp section의 뒤로 각 섹션이 지정됩니다. 병렬 영역의 섹션이 각각 스레드 팀 내의 1개의 스레드로 지정되므로 모든 섹션을 동시에 처리할 수 있습니다. 각 병렬 섹션이 각각 QuickSort를 재귀적으로 호출합니다.
#pragma omp parallel for 구성체의 경우와 같이 섹션이 병렬로 처리되기 위해서는 각 섹션이 다른 섹션에 의존하지 않게 할 필요가 있습니다. 섹션이 공유 자원에의 액세스를 동기 하지 않고 자원을 갱신하면 결과는 보증되지 않습니다.
이 샘플에서는 #pragma omp parallel for와 같이 생략 형식의 #pragma omp parallel sections가 사용되고 있는 것에 주의해 주세요. 또 #pragma omp for 와 같게 #pragma omp sections를 병렬 영역 내에서 단독의 지시문으로서 사용할 수도 있습니다.
그림 5 에 나타낸 구현에 대해서 몇 개인가 주의하는 점을 설명합니다. 우선 병렬 섹션은 재귀적으로 불려 갑니다. 재귀 호출은 병렬 영역, 구체적으로는 이 경우는 병렬 섹션으로 지원 됩니다. 또 네이스트를 가능하게 하면 프로그램이 재귀적으로 QuickSort를 호출할 때 새로운 스레드가 생성됩니다. 이것은 어플리케이션 프로그래머가 의도적으로 그렇게 하고 있는 경우도 있습니다만 결과적으로 생성되는 스레드는 상당히 많아집니다. 프로그램은 스레드의 수를 억제하기 위해서 네이스트를 불가로 할 수 있습니다. 네이스트가 불가인 경우 이 어플리케이션은 2개의 스레드로 QuickSort를 재귀적으로 호출합니다. 재귀적이어도 2개 이외의 스레드가 생성될 것은 없습니다.
게다가 이 어플리케이션을 /openmp 스윗치 없이 컴파일 하면 완전하게 정상적인 차례차례로 구현이 생성 됩니다. OpenMP를 해석하지 않는 컴파일러와의 공존은 OpenMP의 이점의 하나 입니다.
동기 프라그마
복수의 스레드가 동시에 실행되는 경우 대부분은 하나의 스레드가 다른 스레드와 동기를 잡을 필요가 있습니다. OpenMP는 다양한 상황에 대응할 수 있도록 몇 개의 종류의 동기를 제공하고 있습니다.
그 하나가 바리어 동기입니다. 각 병렬 영역의 마지막에는 그 병렬 영역 내의 모든 스레드에 대한 암묵의 바리어 동기가 존재합니다. 바리어 동기는 반드시 모든 스레드가 이 포인트에 도달하고 나서 다음으로 진행되도록 합니다.
#pragma omp for, #pragma omp single, #pragma omp sections의 각 블록의 마지막에도 암묵의 바리어 동기가 존재합니다. 이러한 3개의 타입의 워크 쉐어링 블록으로부터 바리어 동기를 제거하려면 다음과 같이 합니다.
#pragma omp parallel
{
#pragma omp for nowait
for(int i = 1; i < size; ++i)
x[i] = (y[i-1] + y[i+1])/2;
}
여기에서 알듯이 워크 쉐어링 지시문의 nowait 구로 스레드가 for 루프의 최후에서 동기 하지 않는 것을 지시하고 있습니다. 다만 스레드는 병렬 영역의 최후에서는 동기 합니다.
또 하나의 타입은 명시적인 바리어 동기입니다. 병렬 영역의 최후 이외에 바리어 동기를 배치하는 경우가 있습니다. 이 경우는 #pragma omp barrier라고 하는 코드로 배치합니다.
크리티컬 섹션은 바리어로서 사용할 수 있습니다. Win32 API에서는 크리티컬 섹션에 EnterCriticalSection 및 LeaveCriticalSection에 의해서 출입합니다. OpenMP는 #pragma omp critical [name]로 같은 기능을 제공합니다. 이 시멘틱스는 Win32의 크리티컬 섹션과 같으며 수면 아래에서 EnterCriticalSection가 사용됩니다. 이름 첨부의 크리티컬 섹션을 사용할 수 있습니다. 이 경우 코드의 블록은 같은 이름의 다른 크리티컬 섹션이라는 보고 서로 배타적으로 됩니다 (이것은 전 프로세스에 걸쳐서 적용됩니다). 이름을 지정하지 않는 경우는 어떠한 유저 미지정의 이름을 붙일 수 있습니다. 이름 지정 없는 크리티컬 섹션은 모두에게 상호 배타적인 영역입니다.
병렬 영역에서는 코드의 섹션에의 액세스를 특정의 단일의 스레드에 제한하는 일이 자주 있습니다. 예를 들어 병렬 영역 안에서 파일에 쓰는 경우 등입니다. 이러한 경우 많게는 이 코드를 실행하는 스레드가 1개이면 그것이 어느 스레드라도 상관하지 않습니다. OpenMP에는 이것을 실현한 #pragma omp single이 있습니다.
다만 병렬 영역의 코드의 섹션을 이 single 지시문을 사용해 단일 스레드 실행으로 지정하는 것 만으로는 불충분한 경우도 있습니다. 자주 있는 것은 반드시 마스터 스레드가 코드의 섹션을 실행하는 스레드가 되도록 하는 것입니다. 예를 들어 마스터 스레드가 GUI 스레드 그 GUI 스레드로 처리를 실시할 필요가 있는 경우 등입니다. 이것은 #pragma omp master러 실시합니다. single과 달리 마스터 블록의 최초와 마지막에는 암묵의 바리어는 없습니다.
메모리 펜스는 #pragma omp flush로 구현 됩니다. 이 지시문은 프로그램에 메모리 펜스를 작성합니다. 이것은 _ReadWriteBarrier 그것의 동작과 실제로는 동등합니다.
스레드 팀내의 모든 스레드가 OpenMP 프라그마에 같은 순서로 도달할(또는 일절 도달하지 않는) 필요가 있는 것에 주의해 주세요. 즉 다음의 코드는 올바르지 않고 런타임의 동작은 보증되지 않습니다( 이 경우에 대해서는 크래쉬나 정지가 통상의 정도를 넘을 것은 없습니다) .
#pragma omp parallel
{
if(omp_get_thread_num() > 3)
{
#pragma omp single // 모든 스레드에 의해서 액세스 되지 않는다
x++;
}
}
실행 환경 루틴
OpenMP에는 여기까지 설명해 온 프라그마 외에 OpenMP 어플리케이션을 작성하는데 있어서 유용한 실행 루틴이 있습니다. 사용할 수 있는 루틴은 크게 나누어 실행 환경 루틴, 락 및 동기 루틴 및 타이밍 루틴의 3 종류입니다.(이 기사에서는 타이밍 루틴에 대해서는 생략 합니다.) OpenMP의 모든 런타임 루틴은 omp_ 로 시작되어 헤더 파일 omp.h에서 정의됩니다.
실행 환경 루틴이 제공하는 기능에 의해 OpenMP의 가동 환경의 다양한 설정을 조회하거나 변경하거나 할 수 있습니다. omp_set_으로 시작되는 함수는 병렬 영역의 외측에서만 호출할 수 있습니다. 그 외의 모든 함수는 병렬 영역 및 비 병렬 영역의 양쪽 모두로 사용할 수 있습니다.
스레드 팀 내의 스레드 수를 취득 또는 설정하려면 omp_get_num_threads 및 omp_set_num_threads의 루틴을 사용합니다. omp_get_num_threads는 현재의 스레드 팀 내의 스레드 수를 돌려줍니다. 병렬 영역에 없는 스레드로 호출하면 1이 돌려주어집니다. omp_set_num_thread는 스레드가 다음에 도달하는 병렬 영역에서 사용되는 스레드의 수를 설정합니다.
그러나 이것으로 스레드수가 완전하게 정해지는 것은 아닙니다. 병렬 영역에서 사용되는 스레드 수는 이외에도 OpenMP 환경의 2개의 설정, 동적 스레드 및 네이스트에 영향을 받습니다.
동적 스레드는 Boolean 프롭퍼티로 기정에서는 무효입니다. 이 프롭퍼티가 무효 상태로 스레드가 병렬 영역에 도달하면 OpenMP 런타임은 omp_get_max_threads로 지정된 스레드 수로 스레드 팀을 작성합니다. omp_get_max_threads는 기정으로 컴퓨터의 하드웨어 스레드의 수 또는 OMP_NUM_THREADS 환경 변수의 값으로 설정되어 있습니다. 동적 스레드가 유효한 경우 OpenMP 런타임은 omp_get_max_ threads를 넘지 않는 가변의 수의 스레드로 스레드 팀을 작성합니다.
또 하나의 네이스트도 Boolean 프롭퍼티로 기정에서는 무효입니다. 병렬 영역의 네이스트는 이미 병렬 영역 내에 있는 스레드가 다른 병렬 영역에 도달했을 경우에 발생합니다. 네이스트가 유효한 경우 새로운 스레드 팀은 앞의 동적 스레드의 섹션으로 지정한 룰에 따라서 형성됩니다. 네이스트가 무효의 경우 스레드 팀은 1개의 스레드로 형성됩니다.
동적 스레드 및 네이스트의 스테이터스는 런타임 루틴 omp_set_dynamic, omp_get_dynamic, omp_set_nested 및 omp_get_nested를 사용하고 조회 및 설정할 수 있습니다. 각 스레드도 그 환경을 조회할 수 있습니다. 스레드는 스레드 팀내로부터 omp_get_thread_num 호출에 의해서 자신의 스레드 번호를 인식할 수 있습니다. 이것은 Windows의 스레드 ID가 아니고 0으로부터 omp_get_num_threads 보다 1 적은 값이 됩니다.
스레드는 omp_in_parallel에 의해서 정상적으로 병렬 영역 내에서 실행되고 있는지를 인식할 수 있습니다. omp_get_num_procs로 스레드가 컴퓨터의 프로세서수를 인식할 수 있습니다.
다양한 환경 루틴과의 교환을 명확하게 하기 위해 그림 6 을 봅시다. 이 샘플에는 4개의 별개의 병렬 영역과 2개의 네이스트 한 병렬 영역이 있습니다.
통상의 듀얼 프로세서 컴퓨터로 실행하면 Visual Studio 2005를 사용해 컴파일 된 프로그램은 이 코드로부터 다음과 같은 출력을 실시합니다.
Num threads in dynamic region is = 2
Num threads in non-dynamic region is = 10
Num threads in nesting disabled region is = 1
Num threads in nesting disabled region is = 1
Num threads in nested region is = 2
Num threads in nested region is = 2
최초의 병렬 영역에서는 동적 스레드를 유효하게 하고 스레드 수를 10으로 설정해 있습니다. 프로그램의 출력으로부터 동적 스레드가 유효한 경우 OpenMP 런타임은 스레드 팀에 할당하는 스레드를 2개만으로 결정하고 있는 것을 알 수 있습니다. 이것은 컴퓨터에 2개의 프로세서가 탑재되고 있기 때문입니다. 2번째의 병렬 영역에서는 동적 스레드가 무효이기 때문에 OpenMP가 10개의 스레드를 스레드 팀에 할당하고 있습니다.
3번째 및 4번째의 병렬 영역에서는 네이스트 설정의 유효 또는 무효의 영향을 확인할 수 있습니다. 병렬 영역 3에서 네이스트를 무효로 하고 있습니다. 이 경우 네이스트 된 병렬 영역에는 새로운 스레드가 작성되지 않습니다. 즉 네이스트 외측의 병렬 영역 및 네이스트 된 병렬 영역의 양쪽 모두에 대해서 합계로 2개의 스레드가 됩니다. 병렬 영역 4에서는 네이스트를 유효하게 하고 있습니다. 네이스트 된 병렬 영역은 2개의 스레드를 가지는 스레드 팀을 작성합니다(합계로 네이스트 된 병렬 영역에는 4개의 스레드가 존재합니다). 네이스트 된 병렬 영역 마다 스레드가 배증하는 이 프로세스는 스택 스페이스를 다 사용할 때까지 계속 됩니다. 실제로는 수백개의 스레드를 생성할 수 있습니다만 오버헤드가 퍼포먼스의 이익을 간단하게 웃돌 가능성도 있습니다.
또 병렬 영역 3 및 4에서는 동적 스레드가 유효합니다. 다음과 같이 동적 스레드를 무효로 해 코드를 실행하면 어떻게 될까요.
omp_set_dynamic(0);
omp_set_nested(1);
omp_set_num_threads(10);
#pragma omp parallel
{
#pragma omp parallel
{
#pragma omp single
printf("Num threads in nested region is = %d\n",
omp_get_num_threads());
}
}
이하가 예상되는 동작입니다. 최초의 병렬 영역은 10 개의 스레드를 가지는 스레드 팀에서 개시합니다. 거기에 계속 되는 네이스트 된 병렬 영역은 외측의 병렬 영역의 10 개의 스레드에 대해서 각각 10 개의 스레드로 개시합니다. 즉 네이스트 된 병렬 영역 내에서는 합계 100개의 스레드가 실행되어 출력은 다음과 같이 됩니다.
Num threads in nested region is = 10
Num threads in nested region is = 10
Num threads in nested region is = 10
Num threads in nested region is = 10
Num threads in nested region is = 10
Num threads in nested region is = 10
Num threads in nested region is = 10
Num threads in nested region is = 10
Num threads in nested region is = 10
Num threads in nested region is = 10
동기 및 락의 루틴
OpenMP 에는 코드의 동기를 지원하는 런타임 루틴이 있습니다. 락에는 단순,및 네이스트 가능의 2 종류가 있습니다. 각각은 초기화 끝난, 락 상태 및 락 해제 상태의 3 개의 몇 개의 스테이터스로 존재합니다.
단순 락은 (omp_lock_t)에서는 같은 스레드로부터여도 여러 차례 잠글 수 없습니다. 네이스트 가능 락 (omp_nest_lock_t) 는 단순 락과 같습니다만 스레드가 이미 락 되고 있는 락을 설정하려고 해도 그것은 중지 되지 않습니다. 게다가 네이스트 가능한 락은 그것이 몇 회 락 되었는지를 세어 그 회수를 보관 유지하는 레퍼런스이기도 합니다.
동기 루틴은 이러한 락에 근거해 동작합니다. 각각의 루틴에 단순 락 변형 및 네이스트 가능 락 변형이 있습니다. 락으로 실행할 수 있는 액션에는 초기화, 설정 (락의 취득), 해제(락의 해제), 테스트 및 파기의 5개가 있습니다. 이것들은 모두 Win32 크리티컬 섹션 루틴에 엄밀하게 매핑 되고 있어 실제로는 OpenMP는 이러한 루틴의 새로운 래퍼로서 구현 됩니다. 그림 7 에 OpenMP 루틴과 Win32 루틴의 매핑을 나타냅니다.
개발자는 동기 런타임 루틴 또는 동기 프라그마의 어느 쪽을 사용할지 선택할 수 있습니다. 프라그마의 장점은 극히 구조화 되고 있는 점입니다. 이 때문에 이해하기 쉽고 프로그램을 보고, 동기 되는 영역의 시작과 마지막의 위치를 판단하는 것도 간단하게 됩니다.
런타임의 장점은,그 유연성입니다. 락을 다른 함수에 보내 그 함수 안에서 락을 설정하거나 해제하거나 할 수 있습니다. 이것은 프라그마에서는 할 수 없습니다. 일반적으로 런타임 루틴에서만 사용할 수 있는 유연성을 필요로 하지 않는 경우는 동기 프라그마를 사용합니다.
데이터 구조 해석의 병렬 실행
그림 8 의 코드에 병렬인 for 루프를 실행하는 예를 2개 나타내 보입니다. 이 루프에서는 런타임은 최초로 루프에 도달했을 때에 지금부터 실행하는 반복의 회수를 인식하고 있지 않습니다. 최초의 예는 STL std::vector 컨테이너의 해석입니다. 2번째는 표준적인 링크의 리스트입니다.
샘플의 STL 컨테이너의 부분에서는 스레드 팀의 모든 스레드가 for 루프를 실행하여 각 스레드가 각각 반복자를 보관 유지합니다. 그러나 반복하는 1개의 스레드만이 1개의 블록에 들어갑니다(이것이 single의 시멘틱스입니다). OpenMP 런타임은 1개의 블록이 1회만 및 반복하러 1회만 실행되도록 제어합니다. 이 반복 방법은 런타임의 오버헤드가 커지기 때문에 process의 함수로 많은 처리가 발생하는 경우에게만 유용합니다. 링크 리스트의 예도 이것과 같은 논리를 사용하고 있습니다.
또 STL 벡터의 부분에서 루프에 들어가기 전에 필요한 반복의 회수를 판단하기 위해서 std::vector.size를 사용하도록 코드를 고쳐 쓸 수 있습니다. 이것에 의해 루프가 정규화된 OpenMP for 루프의 형식으로 할 수 있습니다. 이것을 다음의 코드에 나타냅니다.
#pragma omp parallel for
for(int i = 0; i < xVect.size(); ++i)
process(xVect[i]);
이 방법은 OpenMP 런타임의 오버헤드가 보다 적기 때문에 배열, 벡터 및 정규화된 OpenMP for 루프의 형식에서 처리할 수 있는 다른 모든 컨테이너에 대해서 이 방법을 추천 합니다.
확장 스케줄링 알고리즘
기정에서는 OpenMP의 병렬화 된 for 루프는 static 스케줄링이라고 하는 알고리즘으로 스케줄 됩니다. 이것은 스레드 팀의 각 스레드에 같은 개수의 반복이 지정되는 것을 의미합니다. n 개의 반복이 있어, 스레드 팀내에 T 개의 스레드가 존재하는 경우 각 스레드는 n/T 개의 반복을 실행합니다(물론 OpenMP는 n 이 T 로 나뉘어 떨어지지 않는 경우에서도 정상적으로 처리합니다) . OpenMP의 스케줄링에는 이외에도 많은 상황에 대응할 수 있도록 dynamic, runtime 및 guided의 메카니즘이 있습니다.
이러한 몇 개의 스케줄링 메카니즘을 지정하려면 #pragma omp for 또는 #pragma omp parallel for 지시문에 schedule 구문을 사용합니다. schedule 구문은 형식은 다음과 같습니다.
schedule(schedule-algorithm[, chunk-size])
이하에 프라그마의 사용의 예를 듭니다.
#pragma omp parallel for schedule(dynamic, 15)
for(int i = 0; i < 100; ++i)
{ ...
#pragma omp parallel
#pragma omp for schedule(guided)
dynamic 스케줄링은 각 스레드가 chunk-size로 지정된 스레드의 수를 실행하도록 스케줄 합니다(chunk-size가 지정되지 않는 경우는 기정으로 1이 지정됩니다) . 스레드는 지정된 반복의 실행이 종료하면 chunk-size의 개수 세트로 다음의 반복을 요구합니다. 이것이 모든 반복이 완료할 때까지 계속 됩니다. 반복의 마지막 세트의 개수는 chunk-size 보다 적게 되는 경우가 있습니다.
guided 스케줄링은 각 스레드가 다음의 식에 따라서 스레드의 수를 실행하도록 스케줄 합니다.
iterations_to_do = max(iterations_not_assigned/omp_get_num_threads(),
chunk-size)
스레드는 지정된 반복의 실행이 종료하면 다음의 반복을 iterations_to_do 식에 근거한 개수세트로 요구합니다. 즉 각 스레드에 할당할 수 있는 반복의 개수는 서서히 적게 됩니다. 스케줄 되는 반복의 마지막 세트의 개수는 iterations_to_do 함수로 정의된 값보다 적게 되는 경우가 있습니다.
이하에 100회의 반복을 실시하는 for 루프를 #pragma omp for schedule(dynamic, 15) 를 사용해 4개의 스레드에 스케줄 했을 경우의 처리를 나타냅니다.
스레드 0이 반복 1 부터 15 취득
스레드 1이 반복 16으로부터 30을 취득
스레드 2가 반복 31로부터 45를 취득
스레드 3이 반복 46으로부터 60을 취득
스레드 2가 종료
스레드 2가 반복 61로부터 75를 취득
스레드 3가 종료
스레드 3이 반복 76으로부터 90을 취득
스레드 0이 종료
스레드 0이 반복 91로부터 100을 취득
다음에 100회의 반복을 실시하는 for 루프를 #pragma omp for schedule(guided, 15) 를 사용해 4 개의 스레드에 스케줄 했을 경우의 처리를 나타냅니다.
스레드 0이 반복 1로부터 25를 취득
스레드 1이 반복 26으로부터 44를 취득
스레드 2가 반복 45로부터 59를 취득
스레드 3이 반복 60으로부터 64를 취득
스레드 2가 종료
스레드 2가 반복 65로부터 79를 취득
스레드 3이 종료
스레드 3이 반복 80으로부터 94를 취득
스레드 2가 종료
스레드 2가 반복 95로부터 100을 취득
dynamic 스케줄링 및 guided 스케줄링은 어느쪽이나 각 반복으로의 처리량이 일정이 아닌 경우 또는 프로세서의 속도가 같지 않은 경우에 적절한 메카니즘입니다. static 스케줄링에는 반복의 load sharing을 실시하는 수단선. dynamic 스케줄링 및 guided 스케줄링은 각각의 동작의 특징에 따르고 자동적으로 반복의 load sharing를 실시합니다. 통상 guided 스케줄링이 dynamic 스케줄보다, 스케줄링에 관련하는 오버헤드가 적기 때문에 퍼포먼스가 좋아집니다.
마지막 스케줄링 알고리즘은 runtime 스케줄링입니다. 이것은 실제로는 스케줄링 알고리즘이라고 하는 것보다는 앞에 설명했던 3개의 스케줄링 알고리즘을 동적으로 선택하는 수법입니다. schedule 구에 runtime이 지정되었을 경우 OpenMP 런타임은 그 for 루프에 OMP_SCHEDULE 환경 변수로 지정된 스케줄링 알고리즘을 사용합니다 . OMP_SCHEDULE 환경 변수의 형식은,type[,chunk size] 입니다.다음과 같이 합니다.
set OMP_SCHEDULE=dynamic,8
runtime 스케줄링을 사용하는 것으로써 엔드 유저가 사용되는 스케줄링의 종류를 어느 정도 자유롭게 결정할 수 있습니다. 기정의 설정은 static 스케줄링입니다.
OpenMP를 사용하는 판단
어떠한 때에 OpenMP를 사용하면 좋은가를 이해해 두는 것은 어떻게 사용하는지를 이해하는 것 같을 정도로 중요합니다. 일반적으로 이 판단에 임하면 도움이 되는 가이드 라인을 다음에 보요 드립니다.
대상 플랫폼이 멀티 코어 또는 멀티 프로세서인 이러한 경우 어플리케이션이 1개의 코어 또는 1개의 프로세서에 대해서 포화 상태이라면 어플리케이션을 OpenMP로 multi-thread화하는 것으로 많은 경우는 어플리케이션의 퍼포먼스가 향상합니다.
크로스 플랫폼인 어플리케이션인 OpenMP는 크로스 플랫폼이며 넓게 지원 되고 있는 API 입니다. API는 프라그마에 의해서 구현 되므로 OpenMP 표준을 인식하지 않는 컴파일러에서도 어플리케이션은 컴파일 할 수 있습니다.
루프가 병렬 가능한 OpenMP는 루프의 병렬화에 가장 적합합니다. 루프의 반복에 의존이 없는 루프가 어플리케이션에 포함되는 경우에는 OpenMP의 사용은 최적인 선택이 됩니다.
마지막 최적화가 필요한 OpenMP는 어플리케이션의 설계 변경을 필요로 하지 않기 때문에 부가적인 퍼포먼스 개선을 위해서 외측으로부터 조금 변경하는데 최적인 툴입니다.
그러나 OpenMP는 multi-thread에 관한 모든 문제를 해결하는 것을 목적과는 하고 있지 않습니다. OpenMP의 성질에는 몇 개의 경향이 있습니다. OpenMP는 하이 퍼포먼스 컴퓨팅 분야에서의 사용을 대상으로 해 개발되고 있으므로 공유 배열의 데이터를 처리하는 루프의 부하가 커지는 프로그래밍으로의 사용에 가장 적합합니다.
보통 스레드의 작성에 코스트가 들도록 OpenMP 병렬 영역의 작성에도 코스트가 듭니다. OpenMP으로 퍼포먼스를 향상시키려면 영역을 병렬화하는 것에 의해서 얻을 수 있는 고속화가 스레드 팀의 개시에 걸리는 코스트를 웃돌지 않으면 안됩니다. Visual C++ 구현은 최초의 병렬 영역에 도달했을 때에 스레드 팀을 작성합니다. 그 다음은 스레드 팀은 다시 필요할 때까지 정지 되고 있습니다. 이 수면 아래에서 OpenMP는 Windows 스레드 풀을 사용합니다. 그림 9 에 OpenMP를 사용하는 2개의 프로세서의 시스템으로 처리를 실시했을 때의 반복의 회수에 의한 가속의 모습을 나타냅니다. 이 실험에는 기사의 처음에서 소개한 간단한 프로그램을 사용하고 있습니다. 퍼포먼스의 피크는 1.7x의 근처입니다. 이것은 2개의 프로세서 시스템으로의 일반적인 값입니다.(퍼포먼스의 차이를 보다 확실히 하기 위해 y 축으로는 순차 처리의 퍼포먼스와 병렬처리의 퍼포먼스의 비율을 표시하고 있습니다.) 반복이 5,000 회를 넘은 근처에서 병렬 버전의 속도가 순차 버전과 같게 되어 있습니다. 그러나 이것으로는 아직 최악의 케이스입니다. 루프의 병렬을 늘리면 보다 적은 회수로 순차 버전의 속도보다 빨라집니다. 이것은 단순하게 각 반복이 실행하는 처리의 양에 의해서 정해집니다. 그러나 이 그래프는 퍼포먼스의 측정이 왜 중요한가를 나타내고 있습니다. OpenMP의 사용은 퍼포먼스의 향상을 보증하는 것이 아닙니다.
그림 9 2개의 프로세서로의 순차와 병렬의 퍼포먼스
OpenMP 프라그마는 사용하는 것은 간단합니다만 에러가 발생했을 경우의 피드백에는 별로 우수하지 않습니다. 예를 들어 미션크리티칼한 어플리케이션을 작성하여 결함이 있는 경우에는 그것이 검출되어 한층 더 충분히 recover 되도록 할 필요가 있는 경우에는 아마 OpenMP는 적합한 툴이 아닙니다 (적어도 OpenMP의 현재의 형태에서는 그렇습니다). 예를 들어 OpenMP가 병렬 영역의 스레드를 작성할 수 없는 경우 또는 크리티컬 섹션을 작성할 수 없는 경우 그 동작은 정의되고 있지 않습니다. Visual C++ 2005에서는 OpenMP 런타임은 최종적으로 이것을 회피하기 전에 당분간 트라이를 반복합니다. OpenMP의 향후의 버전에 요구되는 하나가 에러를 리포트하는 메카니즘입니다.
또 하나 주의할 필요가 있는 것은 Windows 스레드를 OpenMP 스레드와 함께 사용하는 경우입니다. OpenMP 스레드는 Windows 스레드 위에 작성되므로 같은 프로세스 내에서도 정상적으로 실행할 수 있습니다. 문제는 OpenMP가 작성하지 않은 Windows 스레드를 OpenMP가 인식하지 않는 것입니다. 이것에 의해 2 개의 문제가 발생합니다. OpenMP 런타임은 다른 Windows 스레드를 "count"의 일부로 유지하지 않습니다. 또 OpenMP 동기 루틴은 Windows 스레드가 스레드 팀에 포함되지 않기 때문에 Windows 스레드를 동기 하지 않습니다.
어플리케이션에서 OpenMP를 사용할 때의 주의점
OpenMP에서는 간단하게 어플리케이션에 병렬처리를 추가할 수 있습니다만 주의해야 할 점이 몇 개인가 있습니다. 가장 외측에 있는 루프의 인덱스 변수는 프라이빗입니다만 네이스트 되고 있는 루프의 인덱스 변수는 기정으로 공유입니다. 루프를 네이스트 시키는 경우 통상은 내부의 루프의 인덱스 변수를 프라이빗으로 합니다. 이러한 변수를 지정할 경우에는 private 구문을 사용해 주세요.
OpenMP 어플리케이션에서는 C++ 예외를 throw 할 경우에 주의할 필요가 있습니다. 구체적으로는 어플리케이션이 병렬 영역에서 예외를 throw 하는 경우 그것은 같은 병렬 영역의 같은 스레드에 의해서 처리될 필요가 있습니다. 즉 예외로 영역을 나와야 하는 것이 아닙니다. 일반적으로는 병렬 영역에서 예외의 가능성이 있는 경우에는 그 캐치를 존재시킬 필요가 있습니다. 예외가 throw 된 병렬 영역에서 캐치 되지 않는 경우 어플리케이션은 아마 정지 합니다.
구조화 블록을 개시한 #pragma omp <directive> [clause] 스테이트먼트의 최후는 개행이며 괄호가 아닙니다. 괄호로 끝나는 지시문은 컴파일러 에러가 됩니다. 다음에 예를 나타냅니다.
//
잘못되어 있는 괄호
#pragma omp parallel
{
// 코드 (컴파일 되지 않는다)
}
// 올바른 괄호
#pragma omp parallel
{
// 코드
}
Visual Studio 2005에서의 OpenMP 어플리케이션의 디버그는 복잡하게 되는 경우가 있습니다. 특히 F10 및 F11로의 병렬 영역에 대한 스텝 인 및 스텝 오버는 너무 사용하기 쉽다고는 말할 수 없습니다. 이것은 컴파일러가 런타임의 호출 및 스레드 팀의 개시를 실시하기 위해서 코드를 추가하기 때문입니다. 디버거는 이것을 인식하고 있지 않았기 때문에 프로그래머에게는 기묘한 움직임으로 보입니다. 병렬 영역에 breakpoint를 두어 F5를 사용해 거기까지 가는 것을 추천합니다. 병렬 영역을 빠지려면 병렬 영역의 밖에 breakpoint를 두어 F5를 사용합니다.
병렬 영역 내에서는 디버거 "Threads Window" 에 스레드 팀에서 실행되고 있는 다양한 스레드가 표시됩니다. 스레드 ID는 OpenMP 스레드 ID와는 관계가 없습니다만 OpenMP 스레드는 Windows 스레드 위에 작성되므로 그 Windows 스레드 ID를 반영하고 있습니다.
OpenMP는 현재 Profile Guided Optimization(PGO)에는 서포트되고 있지 않습니다. OpenMP는 프라그마에 근거하고 있으므로 /openmp 또는 PGO의 양쪽 모두로 어플리케이션을 컴파일 하여 어느 방법의 퍼포먼스가 가장 높은가를 판단할 수 있습니다.
OpenMP (와)과 .NET
하이 퍼포먼스 컴퓨팅과 .NET이라고 하는 2개 말은 별로 이미지가 연결되지 않습니다만 Visual C++ 2005는 이 분야에 있어 크게 진보하고 있습니다. 이 하나로서 OpenMP가 매니지 C++ 코드를 처리할 수 있게 되었습니다. /openmp 스윗치는 /clr 및 /clr:OldSyntax에 대응하고 있습니다. 즉 OpenMP를 사용해 .NET 타입의 메소드로부터 병렬처리를 실시할 수 있어 이것은 가베지 콜렉션의 대상이 됩니다. /openmp는 현재는 /clr:safe 또는 /clr:pure 에 대응하고 있지 않는 것에 주의해 주세요. 그러나 향후는 대응할 예정입니다.
OpenMP와 매니지 코드의 사용에 대해서는 주의해야 할 제한이 있습니다. OpenMP를 사용하는 어플리케이션은 단일 어플리케이션 도메인의 프로그램 내에서만 사용될 필요가 있습니다. 이미 로드 되고 있는 OpenMP 런타임으로 다른 어플리케이션 도메인이 프로세스에 로드 되면 어플리케이션이 정지 할 가능성이 있습니다.
OpenMP는 어플리케이션을 병렬화하기 위한 간단하고 강력한 테크놀러지입니다. 데이터 처리 루프나 데이터의 함수 블록을 병렬화할 수 있습니다. 기존의 어플리케이션에 간단하게 짜넣을 수 있어 컴파일러 스윗치만으로 사용할지, 하지 않을까를 교체됩니다. OpenMP를 사용하면 간단하게 멀티 코어 CPU의 처리 성능을 충분히 활용할 수 있습니다. 상세한 것에 대하여는 OpenMP의 사양을 참조하는 것을 강하게 추천합니다. multi-thread를 즐겨 봅시다.
|