소개. 컴퓨팅 시스템. OpenMP 이데올로기. 루프 이외의 구문에서의 병렬 처리

OpenMP를 사용하면 공유 메모리 시스템(멀티 코어 및 멀티 프로세서)용 병렬 프로그램을 쉽게 만들 수 있습니다. 이 기사에서는 가장 일반적인 프로그래밍 환경에서 OpenMP를 활성화하는 방법에 대해 설명합니다. 비주얼 스튜디오. 공식 Microsoft 릴리스에 따르면 OpenMP는 Visual Studio 2005/2008/2010 개발 환경의 Professional 버전에서만 지원됩니다. 그러나 무료 Visual Studio Express에는 Professional 버전과 동일한 컴파일러가 있습니다. 따라서 파일을 약간 수정하면 병렬 OpenMP 프로그램이 컴파일되고 가장 중요한 것은 Visual Studio Express에서도 작동합니다.

Microsoft의 OpenMP는 다음 구성 요소를 통해 구현됩니다.

  1. Visual Studio에 포함된 C++ 컴파일러;
  2. 헤더 파일 omp.h;
  3. 컴파일 단계 라이브러리: vcomp.lib 및 vcompd.lib(후자는 디버깅에 사용됨)
  4. 런타임 라이브러리: vcomp90.dll 및 vcomp90d.dll. 이름의 숫자는 다를 수 있습니다. Visual Studio 2005에서는 90 대신 80입니다.

무료 Visual Studio Express에는 나열된 라이브러리가 포함되어 있지 않습니다.

OpenMP와 비주얼 스튜디오 익스프레스

Windows에서 병렬 OpenMP 프로그램을 생성하려는 경우 가장 편리한 방법은 Visual Studio 2005/2008/2010 Professional을 사용하는 것입니다. 학부생과 대학원생에게는 무료라는 점을 상기시켜 드리겠습니다. 또는 Visual Studio Professional을 600달러에 구입할 수 있습니다(물론 세 번째 옵션도 있지만 이에 대해서는 다루지 않겠습니다).

컴퓨터에 Professional 버전이 설치되어 있는 경우 문서의 다음 섹션으로 진행하세요. 이 섹션에서는 어떤 이유로 Visual Studio Express를 사용해야 하는 경우를 고려해 보겠습니다.

Visual Studio Express는 Visual Studio의 간단한 무료 버전입니다. 우리는 2008년 버전에 관심을 가질 것입니다. 여기에서 다운로드할 수 있습니다: http://www.microsoft.com/exPress/download/. 당연히 C++로 프로그래밍할 것이므로 Visual C++ 2008을 선택합니다.

설치 프로그램은 인터넷에서 데이터(약 100MB)를 다운로드하므로 이를 꺼서 대역폭을 일부 절약할 수 있습니다. 마이크로소프트 설치실버라이트와 마이크로소프트 SQL 서버, 필요하지 않은 경우.

Visual Studio Express를 설치한 후 OpenMP 구성 요소를 추가해야 합니다. 합법적인 자유로운 길이렇게 하려면 Windows Server 2008 및 .NET Framework 3.5용 Windows SDK를 설치하세요. Visual Studio는 이 소프트웨어 패키지를 설치하는 동안 업데이트됩니다. 업데이트 프로세스에서는 설치된 Visual Studio 버전(Express 또는 Professional)을 확인하지 않으므로 설치 중에 누락된 구성 요소가 "실수로" 추가됩니다.

OpenMP를 위해서만 Windows SDK를 설치하므로 키트와 함께 제공되는 기가바이트의 문서가 필요하지 않습니다. 다음 요소만 남겨 두는 것이 좋습니다.

그림 1. 필요한 SDK 구성요소

안타깝게도 SDK에는 vcomp90d.dll 라이브러리가 포함되어 있지 않으므로 이 순간 Visual Studio Express에서는 릴리스 모드에서 컴파일된 OpenMP 프로그램만 실행할 수 있습니다. 저는 이 제한 사항을 해결할 수 있는 방법을 찾았습니다. 자세한 내용을 읽어보세요("Visual Studio Express에서 OpenMP 프로그램 디버깅" 섹션).

Visual Studio에서 OpenMP 사용

이전 섹션의 단계를 완료한 후에는 사용하는 Visual Studio 버전은 중요하지 않습니다. 이 개발 환경에서 OpenMP를 지원하는 프로젝트를 생성하는 방법을 단계별로 보여드리겠습니다. 먼저 Visual Studio를 실행하고 파일→새로 만들기→프로젝트...를 선택해야 프로젝트 생성 창이 나타납니다. 프로젝트 유형 "Win32", 템플릿 - "Win32 콘솔 응용 프로그램"을 선택합니다. 의미 있는 프로젝트 이름을 입력하고 프로젝트를 저장할 폴더를 선택한 다음 "솔루션용 디렉터리 만들기"를 선택 취소합니다.

그림 2. 프로젝트 생성 창

“확인” 버튼을 클릭하면 향후 프로젝트를 설정하는 창이 나타납니다. "응용 프로그램 설정" 탭을 선택하고 "빈 프로젝트" 확인란을 선택합니다.

그림 3. 향후 프로젝트 설정 창

“Finish” 버튼을 클릭하면 프로젝트가 생성됩니다. 기본 Visual Studio 창에는 눈에 띄는 변경 사항이 없습니다. 창 제목의 프로젝트 이름만 우리가 프로젝트로 작업하고 있음을 알려줍니다.

이제 프로젝트→새 항목 추가를 클릭하면 프로젝트에 항목을 추가하는 창이 나타납니다. 프로젝트에 .cpp 파일을 추가합니다.

그림 4. 프로젝트에 요소를 추가하는 창

그러면 들어갈 수 있는 창이 뜹니다 소스 코드프로그램들. OpenMP 기능의 다양한 측면을 확인하는 다음 코드에 대한 테스트를 실행합니다.

#포함하다 #포함하다 네임스페이스 std 사용; int main(int argc, char **argv) ( int test(999); omp_set_num_threads(2); #pragma omp 병렬 감소(+:test) ( #pragma omp 중요 cout<< "test = " << test << endl; } return EXIT_SUCCESS; } Листинг 1. Простейшая программа, использующая OpenMP

디버그→디버깅하지 않고 시작을 클릭하여 프로그램을 시작합니다. 모든 것이 올바르게 완료되면 프로그램이 컴파일되고(컴파일할지 여부를 묻는 경우 예를 클릭) 실행되어 test = 999가 인쇄됩니다.

그림 5. 목록 1의 프로그램 결과

"어떻게요?! - 당신은 "결국 프로그램은 0과 2번의 출력을 가져야만 합니다!"라고 말합니다. 사실 OpenMP는 아직 활성화되지 않았으므로 컴파일러는 해당 지시어를 무시했습니다.

OpenMP를 활성화하려면 프로젝트→OMP 속성을 클릭합니다(OMP는 내 예제의 프로젝트 이름입니다). 나타나는 창의 왼쪽 상단에서 "모든 구성"을 선택하고 구성 속성→C/C++→언어 섹션에서 "OpenMP 지원"을 활성화합니다.

그림 6. 프로젝트 속성에서 OpenMP 활성화

그런 다음 디버그 → 디버깅하지 않고 시작을 클릭하여 프로그램을 다시 실행하십시오. 이번에는 프로그램이 test = 0 을 두 번 인쇄합니다.

그림 7. OpenMP가 활성화된 상태에서 목록 1의 프로그램을 실행한 결과

만세! OpenMP가 작동합니다.

메모. Visual Studio Express를 사용하는 경우 현재 구성 "릴리스"를 선택하십시오. 그렇지 않으면 작동하지 않습니다(자세히 읽어보세요).

그림 8. 현재 구성 선택

앞서 언급했듯이 Windows SDK를 설치한 후에도 디버깅에 필요한 vcomp90d.dll 라이브러리가 없기 때문에 아직 Visual Studio Express에서 OpenMP 프로그램을 디버깅할 수 없습니다. 기존 vcomp90.dll 라이브러리를 복사하고 vcomp90d.dll로 이름을 바꾸는 것만으로는 작동하지 않습니다. exe 파일에 포함된 매니페스트에 지정된 체크섬과 버전이 일치하지 않기 때문입니다. 그러므로 우리는 반대쪽에서 "파기"할 것입니다.

디버그 구성에서 컴파일할 때 omp.h 헤더 파일에는 vcompd.lib 라이브러리(있음)가 필요하며, 차례로 vcomp90d.dll(누락)이 필요합니다. 라이선스는 Microsoft의 수정된 헤더 파일을 응용 프로그램에서 사용하는 것을 허용하지 않으므로 omp.h를 수정하는 대신 디버깅 모드가 활성화되어 있다고 추측하지 않도록 다음과 같이 프로그램에 포함시킵니다.

#포함하다 #ifdef _DEBUG #undef _DEBUG #포함 #define_DEBUG #else #include #endif 네임스페이스 std를 사용; int main(int argc, char **argv) ( int test(999); omp_set_num_threads(2); #pragma omp 병렬 감소(+:test) ( #pragma omp 중요 cout<< "test = " << test << endl; } return EXIT_SUCCESS; } Листинг 2. Включаем omp.h «хитрым» способом

위의 작업만으로는 모든 것이 작동하기에는 충분하지 않습니다(지금까지는 프로그램에 내장된 매니페스트만 수정했습니다). 사실 디버깅 모드의 Visual Studio는 OpenMP가 활성화되어 있기 때문에 vcomp90d.dll이 필요한 vcompd.lib를 자동으로 연결합니다. 이 문제를 해결하려면 프로젝트 설정으로 다시 이동하고(프로젝트→OMP 속성) 이번에는 구성: "디버그"를 선택합니다. 구성 속성→링커→입력 섹션에서 vcompd.lib는 링크할 필요가 없지만 vcompd.lib는 다음을 수행하도록 지정합니다.

그림 9. 프로젝트 속성에서 라이브러리 교체

이제 디버깅이 작동하는지, 프로그램이 실제로 병렬로 실행되는지 확인해 보겠습니다. 변수 값을 표시하는 줄에 중단점을 배치합니다. 이렇게 하려면 소스 코드 왼쪽에 있는 회색 막대를 마우스 왼쪽 버튼으로 클릭하세요.

그림 10. 중단점

그런 다음 디버깅 모드에서 프로그램을 실행합니다: 디버그→디버깅 시작(현재 "디버그" 구성을 반환하는 것을 잊지 마십시오. 그림 8 참조). 프로그램이 시작되고 중단점에서 즉시 중지됩니다. "스레드" 탭에서 프로그램이 실제로 두 개의 스레드를 사용하여 실행되는 것을 볼 수 있습니다.

그림 11. Visual Studio Express에서 OpenMP 프로그램 디버깅

라이브러리는 활발하게 개발 중이며 현재 표준은 버전 4.5(2015년 출시)이지만 버전 5.0이 이미 개발 중입니다. 동시에 Microsoft C++ 컴파일러는 버전 2.0만 지원하고 gcc는 버전 4.0을 지원합니다.

OpenMP 라이브러리는 수학적 계산에 자주 사용됩니다. 프로그램을 매우 빠르고 큰 어려움 없이 병렬화할 수 있습니다. 동시에 OpenMP 이데올로기는 서버 소프트웨어 개발에 그다지 적합하지 않습니다(이에 더 적합한 도구가 있습니다).

많은 책과 기사가 이 도서관에 헌정되었습니다. 가장 인기 있는 책은 Antonov와 Gergel의 책입니다. 저는 또한 이 주제에 대한 여러 기사(피상적)를 썼고 포럼에 여러 예제 프로그램도 썼습니다. 제 생각에는 이 책들에는 여러 가지 단점이 있습니다(물론 제가 정정한 부분입니다).

  • 책에서는 OpenMP의 모든 기능을 설명하려고 하며 그 중 대부분은 atavism입니다. 이전 버전의 라이브러리에서는 이것이 필요했지만 이제 이러한 기능을 사용하면 코드가 더 위험해지고 진입 장벽이 높아집니다(다른 프로그래머가 이를 파악하는 데 많은 시간을 소비하게 됩니다). 나는 ""노트에서 이러한 가능성 중 일부를 간략하게 설명했으며 "교과서"에는 이에 대한 내용이 없습니다. 이로 인해 내 교과서는 더 짧고 오래된 정보로 독자에게 과부하를 주지 않습니다.
  • 책에는 "실제" 사례가 거의 없습니다. 최소/최대 검색은 포함되지 않습니다. 나는 독자들에게 그러한 예를 찾아보라고 권유합니다. 포럼에서는 디버깅되고 작동하는 프로그램의 소스 코드를 얻을 수 있으며, 각 프로그램에는 병렬화에 대한 솔루션 및 접근 방식에 대한 자세한 분석이 함께 제공됩니다. 때로는 여러 구현을 찾을 수 있습니다. 예시가 추가됩니다.

컴퓨팅 시스템. OpenMP 이데올로기

병렬 컴퓨팅 시스템에는 멀티 코어/멀티 프로세서 컴퓨터, 클러스터, 비디오 카드 시스템, 프로그래밍 가능 집적 회로 등 다양한 유형이 있습니다. OpenMP 라이브러리는 스레드 병렬성을 사용하여 공유 메모리 시스템을 프로그래밍하는 데에만 적합합니다. 스레드는 단일 프로세스 내에서 생성되며 자체 메모리를 갖습니다. 또한 모든 스레드는 프로세스 메모리에 액세스할 수 있습니다. 이는 그림 1에 개략적으로 표시되어 있습니다.
쌀. OpenMP의 1개 메모리 모델

OpenMP 라이브러리를 사용하려면 "omp.h" 헤더 파일을 포함하고 빌드 옵션 -fopenmp(gcc 컴파일러의 경우)를 추가하거나 프로젝트 설정(Visual Studio의 경우)에서 적절한 플래그를 설정해야 합니다. 프로그램을 시작하면 단일 프로세스가 생성되어 일반 순차 프로그램처럼 실행되기 시작합니다. 병렬 영역(#pragma omp 병렬 지시어로 지정됨)을 발견하면 프로세스는 다수의 스레드를 생성합니다(해당 개수는 명시적으로 설정할 수 있지만 기본적으로 컴퓨팅 코어 시스템에 있는 만큼의 스레드를 생성합니다). . 병렬 영역의 경계는 중괄호로 표시되며 영역의 끝에서 스레드가 삭제됩니다. 이 프로세스는 그림 2에 개략적으로 표시되어 있습니다.


그림 2 omp 병렬 지시어

그림의 검은색 선은 스레드의 수명을 나타내고 빨간색 선은 스레드의 생성을 나타냅니다. 모든 스레드는 프로세스 전체 기간 동안 존재하는 하나의 (메인) 스레드에 의해 생성되는 것을 볼 수 있습니다. OpenMP에서 이러한 스레드를 마스터라고 하며 다른 모든 스레드는 반복적으로 생성되고 소멸됩니다. 병렬 지시문이 중첩될 수 있으며 설정(omp_set_nested 함수에 의해 변경됨)에 따라 중첩 스레드가 생성될 수 있다는 점은 주목할 가치가 있습니다.

OpenMP는 클러스터 아키텍처에서 사용할 수 있지만 독립적으로 사용할 수는 없습니다. 전체 클러스터를 로드할 수 없습니다(이를 위해서는 프로세스를 생성해야 하며 다음과 같은 도구를 사용해야 합니다). MPI). 그러나 클러스터 노드가 멀티 코어인 경우 OpenMP를 사용하면 효율성이 크게 향상될 수 있습니다. 이러한 응용 프로그램은 "하이브리드"가 될 것입니다. 특히 좋은 자료가 많이 작성되었으므로 이 기사에서는 주제에 대해 다루지 않겠습니다.

동기화 - 임계 섹션, 원자, 장벽

병렬 지시어 이전에 생성된 모든 변수는 모든 스레드에 공통됩니다. 스레드 내에서 생성된 변수는 로컬(비공개)이며 현재 스레드에서만 액세스할 수 있습니다. 여러 스레드가 동시에 공유 변수를 변경하면 경쟁 조건이 발생합니다(특정 쓰기 순서와 결과를 보장할 수 없음). 이는 문제이므로 발생하도록 허용해서는 안 됩니다. 한 스레드가 변수를 변경하는 동안 다른 스레드가 변수를 읽으려고 시도하는 경우에도 동일한 문제가 발생합니다. 다음 예에서는 상황을 보여줍니다.

#include "omp.h" #include int main() ( int 값 = 123; #pragma omp 병렬( value++; #pragma omp 중요( std::cout)<< value++ << std::endl; } } }

프로그램은 모든 스레드에 공통적인 값 변수를 정의합니다. 각 스레드는 변수 값을 증가시킨 다음 결과 값을 화면에 인쇄합니다. 듀얼 코어 컴퓨터에서 실행하여 다음과 같은 결과를 얻었습니다.

OpenMP 스레드 경쟁 문제

분명하다 더 자주각 스레드는 먼저 변수 값을 증가시킨 다음 차례로 결과를 인쇄하지만(각각 값을 다시 증가) 경우에 따라 실행 순서가 다릅니다. 이 예에서는 이제 가치를 동시에 높이려고 할 때 관심이 있습니다. 프로그램은 원하는 대로 작동할 수 있습니다. 한 번만 증가하거나 비정상적으로 종료될 수도 있습니다.

문제를 해결하기 위해 중요한 지시문이 있는데, 그 사용 예도 위에 나와 있습니다. 이 예에서 공유 리소스는 메모리(그 안에 배치된 변수)뿐만 아니라 콘솔(스레드가 결과를 출력하는 곳)이기도 합니다. 예제에서는 변수가 증가할 때 경주가 발생하지만 화면에 표시될 때는 발생하지 않습니다. cout 작업은 임계 섹션에 배치됩니다. 한 번에 하나의 스레드만 임계 섹션에 있을 수 있으며 나머지는 해당 스레드가 해제되기를 기다리고 있습니다. 중요한 섹션에 하나의 공유 리소스에 대한 호출만 포함되어 있으면 좋은 경험 법칙으로 간주됩니다. 예에서 섹션은 화면에 데이터를 표시할 뿐만 아니라 증분도 수행합니다. 이는 일반적인 경우에는 좋지 않습니다. ).

여러 작업의 경우 임계 섹션보다 원자 지시문을 사용하는 것이 더 효율적입니다. 동일하게 작동하지만 조금 더 빠르게 작동합니다. 접두사/후위 증가/감소 연산 및 X BINOP = EXPR 과 같은 연산에 사용할 수 있습니다. 여기서 BINOP는 과부하되지 않음연산자 +, *, -, /, &, ^, |,<<, >> . 이러한 지시문을 사용하는 예:

#include "omp.h" #include int main() ( int 값 = 123; #pragma omp 병렬 ( #pragma omp 원자 값++; #pragma omp 중요 (cout) ( std::cout<< value << std::endl; } } }

중요한 섹션의 이름을 지정할 수 있는 가능성도 여기에 표시되어 있습니다. 항상 사용하는 것이 좋습니다. 요점은 이름이 지정되지 않은 모든 섹션은 하나(매우 큰)로 처리되며 명시적으로 이름을 지정하지 않으면 이 섹션 중 하나만 한 번에 하나의 스레드를 갖게 된다는 것입니다. 나머지는 기다릴 것이다. 섹션의 이름은 프로그래머에게 그것이 속한 리소스 유형을 알려주어야 합니다. 주어진 예에서 이러한 리소스는 화면 출력 스트림(cout)입니다.

마지막 예에서 공유 데이터에 대한 각 작업은 임계 섹션에 배치되거나 원자적이지만 문제가 있습니다. 이러한 작업이 수행되는 순서는 아직 명확하지 않습니다. 프로그램을 20번 실행한 결과 '125 125' 화면은 물론이고 '124 125' 화면도 나왔습니다. 각 스레드가 먼저 값을 증가시킨 다음 화면에 인쇄하도록 하려면 Barrier 지시문을 사용할 수 있습니다.

#pragma omp 병렬( #pragma omp 원자 값++; #pragma omp 장벽 #pragma omp 임계(cout)( std::cout<< value << std::endl; } }

계산의 일부를 완료한 스레드는 Barrier 지시문에 도달하고 모든 스레드가 동일한 지점에 도달할 때까지 기다립니다. 마지막 스레드를 기다린 후 스레드는 계속 실행됩니다. 이제 프로그램에는 동기화 문제가 없지만 모든 스레드가 병렬이기는 하지만 동일한 작업(병렬 영역 내부에 설명됨)을 수행하고 있다는 점에 유의하세요. 이 상황에서는 프로그램이 더 빨리 작동하지 않습니다. 해결해야 할 작업을 스레드 간에 분산해야 합니다., 이것은 다양한 방법으로 수행될 수 있습니다...

스레드 간 작업 분할

병렬 루프

OpenMP에서 작업을 분산하는 가장 널리 사용되는 방법은 병렬 루프입니다. 프로그램이 루프를 실행하는 데 거의 전체 수명을 소비한다는 것은 비밀이 아니며, 루프 반복 사이에 종속성이 없으면 루프를 벡터화 가능이라고 합니다(반복은 스레드 간에 분할되어 서로 독립적으로 실행될 수 있음). ""기사에서 배열 요소의 합과 수치 적분 계산의 예를 사용하여 이 구성을 피상적으로 설명했지만 반복하지 않겠습니다. 링크를 따라가는 것이 좋습니다. 다음은 병렬 루프의 보다 "흥미로운" 측면을 설명합니다.

병렬 루프를 사용하면 스레드 간 반복 배포 알고리즘을 변경하는 일정 옵션을 설정할 수 있습니다. 총 3가지 알고리즘이 지원됩니다. 다음으로, n번의 반복을 수행하는 p개의 스레드가 있다고 가정합니다.

계획 옵션:

  • Schedule(static) - 정적 스케줄링. 이 옵션을 사용하면 루프 반복이 스레드 간에 균등하게(대략적으로) 나누어집니다. 0 스레드는 첫 번째 \(\frac(n)(p)\) 반복을 수신하고, 첫 번째 스레드는 두 번째 반복을 수신합니다.
  • Schedule(static, 10) — 반복의 블록 순환 분포입니다. 각 스레드는 루프 시작 시 지정된 반복 횟수를 수신한 다음(남은 반복이 있는 경우) 할당 절차를 계속합니다. 스케줄링은 한 번 수행되며 각 스레드는 실행해야 하는 반복을 "학습"합니다.
  • 일정(동적), 일정(동적, 10) — 동적 계획. 기본적으로 옵션 매개변수는 1입니다. 각 스레드는 지정된 반복 횟수를 수신하고 이를 실행하며 새 부분을 요청합니다. 정적 계획과 달리 반복적으로 수행됩니다(프로그램 실행 중). 스레드 간 반복의 구체적인 분포는 스레드 작업 속도와 반복의 복잡성에 따라 달라집니다.
  • 일정(안내), 일정(안내, 10)은 각 후속 배포에 따라 반복 횟수가 변경되는 동적 계획 유형입니다. 할당은 라이브러리 구현에 따라 일부 초기 크기부터 옵션에 지정된 값(기본값 1)까지 시작됩니다. 할당된 부분의 크기는 할당되지 않은 반복 횟수에 따라 달라집니다.

대부분의 경우 가장 좋은 옵션은 static 입니다. 왜냐하면 분포는 한 번만 수행됩니다. 문제의 반복 복잡성이 크게 다를 경우 이 매개변수를 사용하는 것이 좋습니다. 예를 들어, 주 대각선 아래에 있는 정사각 행렬 요소의 합을 계산하는 경우 static은 최상의 결과를 제공하지 않습니다. 첫 번째 스레드는 훨씬 적은 수의 작업을 수행하고 유휴 상태가 됩니다.

병렬 루프 문서에서는 nowait 및 감소 옵션도 설명합니다. 그 중 첫 번째는 눈에 띄는 이득을 거의 제공하지 않으며 두 번째는 가능한 한 자주(중요 섹션 대신) 사용하는 것이 좋습니다. 해당 기사에서는 모든 예제에서 축소가 사용되었으며 이로 인해 중요한 섹션의 명시적인 사용을 피할 수 있었지만 이것이 항상 가능한 것은 아니므로 그 안에 무엇이 있는지 아는 것이 좋습니다. 따라서 병렬로 다음과 같이 배열 요소의 합을 계산할 수 있습니다.

Int sum_arr(int *a, const int n) ( int sum = 0; #pragma omp 병렬 감소 (+: sum) ( #pragma omp for for (int i = 0; i< n; ++i) sum += a[i]; } return sum; }

보기에는 좋지만 실제로는 배열 일부의 합계를 저장하기 위해 각 스레드에서 지역 변수가 생성되고(계산은 현재 스레드에 할당됨) 값 0이 할당됩니다( + 연산자). 각 스레드는 합계를 계산하지만 최종 결과를 얻으려면 이러한 값을 모두 추가해야합니까? — 이는 대략 다음과 같이 임계 섹션 또는 원자적 연산을 사용하여 수행됩니다.

Int sum_arr(int *a, const int n) ( int sum = 0; #pragma omp 병렬 ( int local_sum = 0; #pragma omp for for (int i = 0; i< n; ++i) local_sum += a[i]; #pragma omp atomic sum += local_sum; } return sum; }

이 접근 방식은 항상 사용되므로 이 코드를 자세히 살펴보는 것이 좋습니다. 약간 더 복잡한 예는 병렬입니다. 자료에 대한 숙달도를 테스트하는 작업으로 히스토그램(예: 이미지)을 작성해 보는 것이 좋습니다.

병렬 작업

병렬 작업은 병렬 루프보다 더 유연한 메커니즘입니다. 병렬 루프는 병렬 영역 내에서 설명되며 문제가 발생할 수 있습니다. 예를 들어, 우리는 1차원 배열 요소의 합을 계산하기 위한 병렬 함수를 작성했고, 이 함수를 사용하여 행렬 요소의 합을 계산하기로 결정했지만 병렬로도 수행했습니다. 결과는 중첩된 병렬성입니다. (이론적으로) 코드가 8개 코어에서 실행되면 실제로 64개의 스레드가 생성됩니다. 글쎄, 누군가가 다른 일을 병렬로 수행한다는 아이디어를 내놓으면 어떨까요?

때로는 이러한 상황을 감지하기가 쉽지 않습니다. 예를 들어 포럼에서 n개의 행렬식을 병렬로 계산하는 병렬 구현을 찾을 수 있습니다. 함수를 호출합니다.

병렬 루프의 문제점은 생성된 스레드 수가 병렬화된 함수와 서로 호출하는 방법에 따라 달라진다는 것입니다. 이 모든 것을 추적하는 것은 매우 어렵고, 유지하는 것은 더욱 어렵습니다. 문제에 대한 해결책은 스레드를 생성하지 않고 대기열에 작업을 추가하기만 하는 병렬 작업입니다. 해제된 스레드는 풀에서 작업을 선택합니다. 기사에서 이 메커니즘을 설명했습니다.
""그리고 반복하지 않겠습니다. (링크의 자료를 읽는 것이 좋습니다. 기사에서는 가능성에 대해 논의합니다. 재귀 함수의 병렬화작업 메커니즘을 사용하여). OpenMP 3.0 표준(2008년)에서 병렬 작업이 제안되었으므로 Microsoft C++에서는 해당 지원을 사용할 수 없다는 점만 참고하겠습니다. 또한 새로운 OpenMP 4.5 표준에서는 작업 루프 구성을 제안했으며, 이로 인해 이제 병렬 작업을 사용하여 루프를 병렬 루프로 병렬화하는 것이 편리해졌습니다.

평행 섹션

병렬 섹션의 메커니즘은 제가 보기에 상당히 낮은 수준인 것 같습니다. 그러나 여러 경우에 유용합니다. 위에서 언급했듯이 병렬 루프는 루프 반복이 서로 독립적인 경우에만 사용할 수 있습니다. 여기서는 할 수 없습니다:

(int i = 1; i< n; ++i) a[i] = a+1;

프로그램에 서로 독립적이지만 자체적으로 종속성을 갖는 여러 조각이 있는 경우 병렬 섹션 메커니즘을 사용하여 병렬화됩니다.

#pragma omp 병렬 ( #pragma omp 섹션 ( #pragma omp 섹션 ( for (int i = 1; i< n; ++i) a[i] = a+1; } #pragma omp section { for (int i = 1; i < n; ++i) b[i] = b+1; } } }

예시가 좋지 않아서... 나는 이것이 필요한 실제 작업을 아직 발견하지 못했습니다. 매우 중요합니다. 섹션을 사용하여 재귀 함수를 병렬화하려고 시도하지 마세요(이를 위해서는 작업을 사용하세요). 아마도 미래에는(특히 Microsoft가 OpenMP 3.0 표준을 구현하는 경우) 이 섹션을 .

결론 및 추가 자료

이것이 튜토리얼이 끝나는 곳입니다... OpenMP를 사용한 소스 코드의 더 많은 예를 보고 싶거나 특정 문제 해결에 대한 질문이 있는 경우 포럼을 살펴보십시오. 기사 내용에 대해 궁금한 점이 있으면 댓글을 작성하세요.

  1. Lazarus/Freepascal의 OpenMP 지원: http://wiki.lazarus.freepascal.org/OpenMP_support
  2. Java용 OpenMP: http://www.omp4j.org/
  3. 안토노프 A.S. 병렬 프로그래밍 MPI 및 OpenMP 기술: 교과서. 용돈. 서문: V.A. Sadovnichy. – M.: 모스크바 대학교 출판사, 2012.-344 p.-(시리즈 “슈퍼컴퓨터 교육”). ISBN 978-5-211-06343-3.
  4. 게르겔 V.P. 멀티코어 멀티프로세서 시스템을 위한 고성능 컴퓨팅. 교과서 - 니즈니노브고로드; UNN의 이름을 딴 출판사. N. I. 로바체프스키, 2010
  5. : https://site/archives/1150
  6. 병렬 OpenMP 작업:
  7. : https://site/forums/forum/programming/parallel_programming/openmp_library
  8. : href=”https://site/forums/topic/openmp-problems
  9. : https://site/forums/topic/matrix-triangulation_cplusplus
  10. 하이브리드 클러스터 애플리케이션 프로파일링 MPI+OpenMP: https://habrahabr.ru/company/intel/blog/266409/
  11. : https://site/forums/topic/openmp-cramer-method_cplusplus
  12. C++ 프로그래밍 시 OpenMP의 32가지 함정: https://www.viva64.com/ru/a/0054/
  13. OpenMP 및 정적 코드 분석: https://www.viva64.com/ru/a/0055/

OpenMP 인프라를 사용하면 C, C++ 및 Fortran에서 병렬 프로그래밍 기술을 효과적으로 구현할 수 있습니다. GCC(GNU Compiler Collection) 버전 4.2는 OpenMP 2.5 사양을 지원하고 GCC 버전 4.4는 최신 OpenMP 3 사양을 지원합니다. Microsoft® Visual Studio를 포함한 다른 컴파일러도 OpenMP를 지원합니다. 이 기사에서는 OpenMP 컴파일러 pragma를 사용하는 방법을 설명합니다. 또한 일부 OpenMP API에 대한 정보가 포함되어 있으며 OpenMP를 사용하는 일부 병렬 컴퓨팅 기술을 다룹니다. 이 기사의 모든 예제는 GCC 4.2 컴파일러를 사용합니다.

일의 시작

OpenMP의 가장 큰 장점은 GCC 컴파일러의 표준 설치를 제외하고 추가 작업이 필요하지 않다는 것입니다. OpenMP 애플리케이션은 -fopenmp 옵션을 사용하여 컴파일해야 합니다.

첫 번째 OpenMP 애플리케이션 생성

간단한 애플리케이션 작성부터 시작해 보겠습니다. 안녕하세요, 월드!추가 pragma가 포함되어 있습니다. 이 애플리케이션의 코드는 목록 1에 표시되어 있습니다.

목록 1. OpenMP를 사용하여 작성된 "Hello World" 프로그램
#포함하다 int main() ( #pragma omp 병렬 ( std::cout<< "Hello World!\n"; } }

g++를 사용하여 이 코드를 컴파일하고 실행하면 화면에 다음 메시지가 표시됩니다. 안녕하세요, 월드!. 이제 -fopenmp 옵션을 사용하여 코드를 다시 컴파일해 보겠습니다. 프로그램 결과는 목록 2에 나와 있습니다.

목록 2. -fopenmp 옵션을 사용하여 코드 컴파일 및 실행
Tintin$ g++ test1.cpp -fopenmptintin$ ./a.out Hello World! 안녕하세요 월드! 안녕하세요 월드! 안녕하세요 월드! 안녕하세요 월드! 안녕하세요 월드! 안녕하세요 월드! 안녕하세요 월드!

무슨 일이에요? -fopenmp 컴파일러 옵션을 사용하면 #pragma omp 병렬 지시문이 작동합니다. 컴파일하는 동안 GCC의 내부는 최적의 시스템 로드 조건(하드웨어 및 운영 체제 구성에 따라 다름)에서 실행될 수 있는 만큼의 병렬 스레드를 생성하며, 생성된 각 스레드는 pragma 뒤의 블록에 포함된 코드를 실행합니다. 이 동작을 암시적 병렬화, OpenMP 코어는 일반적인 코드 조각을 많이 작성하지 않아도 되는 강력한 pragma 세트로 구성됩니다(재미로 POSIX 스레드를 사용하여 동일한 작업을 구현하여 제공된 코드를 비교할 수 있습니다). 저는 각각 2개의 논리 코어로 구성된 4개의 물리적 코어가 있는 Intel® Core i7 프로세서가 탑재된 컴퓨터를 사용하고 있습니다. 이는 목록 2(8스레드 = 8논리 코어)의 결과를 설명합니다.

OpenMP 병렬 기능

스레드 수는 num_threads 인수가 있는 pragma를 사용하여 쉽게 제어할 수 있습니다. 다음은 스레드 수(5개 스레드)가 설정된 목록 1의 코드입니다.

목록 3. num_threads를 사용하여 스레드 수 제어
#포함하다 int main() ( #pragma omp 병렬 num_threads(5) ( std::cout<< "Hello World!\n"; } }

num_threads 인수 대신 대체 방법을 사용하여 코드 실행 스레드 수를 지정할 수 있습니다. 여기서는 omp_set_num_threads라는 첫 번째 OpenMP API를 살펴보겠습니다. 이 함수는 omp.h 헤더 파일에 정의되어 있습니다. 목록 4의 코드를 실행하기 위해 추가 라이브러리를 사용할 필요는 없으며 -fopenmp 옵션만 사용하면 됩니다.

목록 4. omp_set_num_threads를 사용한 세분화된 스레드 제어
#포함하다 #포함하다 int main() ( omp_set_num_threads(5); #pragma omp 병렬 ( std::cout<< "Hello World!\n"; } }

마지막으로 외부 환경 변수를 사용하여 OpenMP의 작동을 제어할 수 있습니다. 목록 2의 코드를 수정하고 다음 문구를 인쇄할 수 있습니다. 안녕하세요 월드!목록 5에 표시된 대로 OMP_NUM_THREADS 변수를 6으로 설정하여 6번 수행합니다.

목록 5. OpenMP 구성을 위한 환경 변수
tintin$export OMP_NUM_THREADS=6tintin$ ./a.out Hello World! 안녕하세요 월드! 안녕하세요 월드! 안녕하세요 월드! 안녕하세요 월드! 안녕하세요 월드!

우리는 OpenMP의 세 가지 측면인 컴파일러 pragma, 런타임 API 및 환경 변수를 살펴보았습니다. 런타임 API와 함께 환경 변수를 사용하면 어떻게 되나요? API의 우선순위가 더 높습니다.

사례 연구

OpenMP는 암시적 병렬화 기술을 사용하고 프라그마, 명시적 함수 및 환경 변수를 사용하여 명령을 컴파일러에 전달할 수 있습니다. OpenMP 사용의 이점을 명확하게 보여주는 예를 살펴보겠습니다. 목록 6에 표시된 코드를 살펴보세요.

목록 6. for 루프의 순차적 처리
int main() ( int a, b; // ... 배열 a와 b에 대한 초기화 코드; int c; for (int i = 0; i< 1000000; ++i) c[i] = a[i] * b[i] + a * b; // ... выполняем некоторые действия с массивом c }

분명히 for 루프는 여러 프로세서 코어에 의해 한 번에 병렬화되고 처리될 수 있습니다. 왜냐하면 모든 요소 c[k]의 값 계산은 배열 c의 나머지 요소에 어떤 식으로든 의존하지 않기 때문입니다. 목록 7에서는 OpenMP를 사용하여 이를 수행하는 방법을 보여줍니다.

목록 7. 병렬 for pragma를 사용하여 for 루프에서 병렬 처리
int main() ( int a, b; // ... 배열 a와 b를 초기화하는 코드; int c; #pragma omp 병렬 for for (int i = 0; i< 1000000; ++i) c[i] = a[i] * b[i] + a * b; // ... выполняем некоторые действия с массивом c }

병렬 for pragma는 for 루프의 작업 부하를 여러 스레드에 분산하는 데 도움이 되며 각 스레드는 별도의 프로세서 코어에서 처리될 수 있습니다. 따라서 전체 계산 시간이 크게 단축됩니다. 이는 목록 8에서 확인됩니다.

목록 8. omp_get_wtime API 함수 사용 예
#포함하다 #포함하다 #포함하다 #포함하다 int main(int argc, char *argv) ( int i, nthreads; clock_t clock_timer; double wall_timer; double c; for (nthreads = 1; nthreads<=8; ++nthreads) { clock_timer = clock(); wall_timer = omp_get_wtime(); #pragma omp parallel for private(i) num_threads(nthreads) for (i = 0; i < 1000000; i++) c[i] = sqrt(i * 4 + i * 2 + i); std::cout << "threads: " << nthreads << " time on clock(): " << (double) (clock() - clock_timer) / CLOCKS_PER_SEC << " time on wall: " << omp_get_wtime() - wall_timer << "\n"; } }

목록 8에서는 스레드 수를 늘리면서 내부 for 루프의 실행 시간을 측정합니다. omp_get_wtime API 함수는 지정된 참조 지점이 시작된 이후 경과된 실제 시간(초)을 반환합니다. 따라서 omp_get_wtime() - wall_timer는 for 루프의 실제 실행 시간을 반환합니다. clock() 시스템 호출은 CPU가 전체 프로그램을 실행하는 데 소요되는 시간을 추정하는 데 사용됩니다. 즉, 최종 결과를 얻기 전에 스레드를 고려하여 이러한 모든 시간 간격을 더합니다. 내 Intel Core i7 컴퓨터에서 목록 9에 표시된 결과를 얻었습니다.

목록 9. 내부 for 루프에 대한 통계
스레드: 시계에 1시간(): 0.015229 벽에 시간: 0.0152249 스레드: 시계에 2시간(): 0.014221 벽에 시간: 0.00618792 스레드: 시계에 3시간(): 0.014541 벽에 시간: 0.00444412 스레드: 4시간 clock(): 0.014666 벽에 걸린 시간: 0.00440478 스레드: 5 시계에 걸린 시간(): 0.01594 벽에 걸린 시간: 0.00359988 스레드: 6 시계에 걸린 시간(): 0.015069 벽에 걸린 시간: 0.00303698 스레드: 7 시계에 걸린 시간(): 0.016365 벽에 걸린 시간: 0.00258303 스레드: 8 시계에 걸린 시간(): 0.01678 벽에 걸린 시간: 0.00237703

CPU 시간(시계의 시간)은 모든 경우에 거의 동일한 것으로 나타났지만(스레드 및 컨텍스트 전환을 생성하는 데 소요되는 추가 시간을 고려하지 않고 그래야 함) 우리가 관심을 갖는 실제 시간(시간 on wall)은 개별 프로세서 코어에 의해 병렬로 실행되는 것으로 추정되는 스레드 수가 증가함에 따라 지속적으로 감소했습니다. 따라서 pragma 구문에 대해 마지막으로 메모해 보겠습니다. #pragma parallel for private(i)는 루프 변수 i가 스레드 로컬 메모리로 처리된다는 의미입니다. 각 스레드에는 이 변수의 자체 복사본이 포함되어 있습니다. 스레드 지역 변수가 초기화되지 않았습니다.

OpenMP의 중요한 코드 섹션

물론 OpenMP가 코드의 중요한 부분을 자동으로 처리한다고 완전히 신뢰할 수는 없다는 점을 이해하고 계실 것입니다. 물론 상호 예외(뮤텍스)를 명시적으로 생성할 필요는 없지만 여전히 중요한 영역을 지정해야 합니다. 구문은 다음 예에 나와 있습니다.

#pragma omp important (선택적 섹션 이름) ( // 2개의 스레드는 이 코드 블록을 동시에 실행할 수 없습니다.)

pragma omp 비판적 지시어 뒤에 오는 코드는 주어진 시간에 하나의 스레드에서만 실행될 수 있습니다. 또한 선택적 섹션 이름은 전역 식별자이며 동일한 식별자를 가진 임계 섹션은 두 스레드에서 동시에 처리할 수 없습니다. Listing 10의 코드를 살펴보자.

목록 10. 동일한 이름을 가진 여러 중요 섹션
#pragma omp 비판적 (섹션1) ( myhashtable.insert("key1", "value1"); ) // ... 여기에는 다른 코드가 포함되어 있습니다 #pragma omp 비판적 (섹션1) ( myhashtable.insert("key2", "value2 "); )

이 목록의 코드를 보면 중요한 영역 이름이 동일하기 때문에 해시 테이블에 대한 두 개의 삽입이 동시에 발생하지 않을 것이라고 가정할 수 있습니다. 이는 많은 잠금을 사용하는 경향이 있는(불필요한 복잡성을 초래할 수 있음) pthread에서 중요한 섹션을 처리할 때 익숙했던 것과 약간 다릅니다.

OpenMP의 잠금 및 뮤텍스

흥미롭게도 OpenMP에는 자체 버전의 뮤텍스가 포함되어 있습니다(결국 OpenMP는 단순한 pragma가 아닙니다). 따라서 omp.h 헤더 파일에 정의된 omp_lock_t 유형을 살펴보세요. API 함수 이름이 동일하더라도 일반 pthread 스타일 뮤텍스 작업은 true로 평가됩니다. 다음은 알아야 할 5가지 API 함수입니다.

  • omp_init_lock: 이 API 함수는 omp_lock_t 유형에 접근할 때 가장 먼저 사용해야 하며 초기화를 위한 것입니다. 초기화 직후 잠금은 초기(설정 해제) 상태가 된다는 점에 유의해야 합니다.
  • omp_destroy_lock: 자물쇠를 파괴합니다. 이 API 함수가 호출되면 잠금은 초기 상태에 있어야 합니다. 이는 omp_set_lock을 호출한 다음 잠금을 해제할 수 없음을 의미합니다.
  • omp_set_lock: omp_lock_t를 설정합니다. 즉, 뮤텍스를 활성화합니다. 스레드가 잠금을 획득할 수 없으면 기회가 제공될 때까지 계속 기다립니다.
  • omp_test_lock: 잠금이 가능한 경우 잠금을 획득하려고 시도합니다. 성공하면 1을, 실패하면 0을 반환합니다. 이 기능은 비차단즉, 스레드가 잠금을 획득할 때까지 기다리도록 강제하지 않습니다.
  • omp_unset_lock: 잠금 상태를 원래 상태로 재설정합니다.

목록 11에는 OpenMP 잠금을 사용하여 여러 스레드를 처리하도록 향상된 레거시 단일 스레드 큐의 간단한 구현이 포함되어 있습니다. 이 예는 최선의 다재다능한 사용 사례가 아니며 단순히 기능을 보여주기 위해 제공된 것입니다.

목록 11. OpenMP를 사용하여 단일 스레드 대기열 개선
#포함하다 #include "myqueue.h" 클래스 omp_q: 공개 myqueue ( 공개: typedef myqueue 베이스; omp_q() ( omp_init_lock(&lock); ) ~omp_q() ( omp_destroy_lock(&lock); ) bool push(const int& value) ( ​​​​omp_set_lock(&lock); bool result = this->base::push(value); omp_unset_lock( &lock); return result; ) bool trypush(const int& value) ( ​​​​bool result = omp_test_lock(&lock); if (result) ( result = result && this->base::push(value); omp_unset_lock(&lock ); ) return result; ) //pop private의 경우에도 마찬가지입니다: omp_lock_t lock; );

중첩된 잠금

OpenMP의 다른 유형의 잠금은 다양한 omp_nest_lock_t 잠금입니다. 이는 omp_lock_t와 유사하지만 잠금을 보유하는 스레드에 의해 잠금을 여러 번 획득할 수 있다는 추가 이점이 있습니다. omp_set_nest_lock을 사용하여 스레드가 중첩된 잠금을 획득할 때마다 내부 잠금 카운터가 증가합니다. omp_unset_nest_lock에 대한 하나 이상의 호출이 내부 잠금 카운터를 0으로 줄이면 보유 스레드에 의해 잠금이 해제됩니다. omp_nest_lock_t 작업에는 다음 API 함수가 사용됩니다.

  • omp_init_nest_lock(omp_nest_lock_t*): 내부 중첩 카운터를 0으로 설정합니다.
  • omp_destroy_nest_lock(omp_nest_lock_t*): 자물쇠를 파괴합니다. 0이 아닌 카운터 값으로 잠금에 대해 이 API 함수를 호출하면 예측할 수 없는 결과가 발생합니다.
  • omp_set_nest_lock(omp_nest_lock_t*): 보유 스레드가 여러 번 호출할 수 있다는 점을 제외하면 omp_set_lock과 유사합니다.
  • omp_test_nest_lock(omp_nest_lock_t*): omp_set_nest_lock API 함수의 비차단 버전입니다.
  • omp_unset_nest_lock(omp_nest_lock_t*): 내부 잠금 카운터가 0이 되면 잠금을 해제합니다. 다른 경우에는 이 API 함수를 호출할 때마다 카운터 값이 감소합니다.

작업 실행에 대한 세부 제어

우리는 pragma omp 병렬 지시문을 따르는 코드 블록이 모든 스레드에 의해 병렬로 처리된다는 것을 이미 살펴보았습니다. 이러한 블록 내의 코드는 지정된 스레드에서 실행될 범주로 나눌 수도 있습니다. Listing 12의 코드를 살펴보자.

목록 12. 병렬 섹션 pragma 사용
int main() ( #pragma omp 병렬 ( cout<< "Это выполняется во всех потоках\n"; #pragma omp sections { #pragma omp section { cout << "Это выполняется параллельно\n"; } #pragma omp section { cout << "Последовательный оператор 1\n"; cout << "Это всегда выполняется после оператора 1\n"; } #pragma omp section { cout << "Это тоже выполняется параллельно\n"; } } } }

pragma omp 섹션 지시문 앞의 코드와 pragma omp 병렬 지시문 바로 뒤의 코드는 모든 스레드에서 병렬로 처리됩니다. pragma omp 섹션 지시문을 사용하여 그 뒤에 오는 코드는 별도의 하위 섹션으로 나뉩니다. 각 pragma omp 섹션 블록은 별도의 스레드로 실행될 수 있습니다. 그러나 섹션 블록 내의 개별 명령문은 항상 순차적으로 실행됩니다. 목록 13은 목록 12의 코드를 실행한 결과를 보여줍니다.

목록 13. 목록 12의 코드 실행 결과
tintin$ ./a.out 모든 스레드에서 실행됨 모든 스레드에서 실행됨 모든 스레드에서 실행됨 모든 스레드에서 실행됨 모든 스레드에서 실행됨 모든 스레드에서 실행됨 모든 스레드에서 실행됨 모든 스레드에서 실행됨 병렬로 실행됨 순차 문 1 병렬로도 실행됩니다. 이는 항상 명령문 1 이후에 실행됩니다.

목록 13에서는 처음에 생성된 8개의 스레드를 다시 볼 수 있습니다. 8개 스레드 중 3개면 pragma omp 섹션 블록을 처리하는 데 충분합니다. 두 번째 섹션에서는 텍스트 출력 문이 실행되는 순서를 지정했습니다. 이것이 섹션 pragma를 사용하는 요점입니다. 필요한 경우 코드 블록이 실행되는 순서를 지정할 수 있습니다.

병렬 루프와 함께 Firstprivate 및 lastprivate 지시문

이 기사에서는 이미 private 지시문을 사용하여 스레드 로컬 메모리를 선언하는 방법을 보여주었습니다. 그러나 스레드 지역 변수를 초기화하는 방법은 무엇입니까? 계속 진행하기 전에 메인 스레드 변수의 값과 동기화할 수 있을까요? 이러한 경우에는 firstprivate 지시문이 유용합니다.

첫 번째 개인 지시어

firstprivate(variable) 지시문을 사용하면 스레드의 변수를 기본 스레드에 있던 값으로 초기화할 수 있습니다. 목록 14의 코드를 살펴보겠습니다.

목록 14. 기본 스레드와 동기화되지 않은 스레드 로컬 변수 사용
#포함하다 #포함하다 int main() ( int idx = 100; #pragma omp 병렬 private(idx) ( printf("스레드 %d idx = %d\n", omp_get_thread_num(), idx); ) )

결과는 다음과 같습니다(결과는 다를 수 있음).

스트림 1에서 idx = 1 스트림 5에서 idx = 1 스트림 6에서 idx = 1 스트림 0에서 idx = 0 스트림 4에서 idx = 1 스트림 7에서 idx = 1 스트림 2에서 idx = 1 스트림 3에서 idx = 1

목록 15에는 firstprivate 지시문을 사용하는 코드가 포함되어 있습니다. 예상대로 출력에는 모든 스레드에서 idx 변수가 100으로 설정되어 있음이 표시됩니다.

목록 15. firstprivate 지시문을 사용하여 스레드 로컬 변수 초기화
#포함하다 #포함하다 int main() ( int idx = 100; #pragma omp 병렬 firstprivate(idx) ( printf("스레드 %d idx = %d\n", omp_get_thread_num(), idx); ) )

또한 omp_get_thread_num() 메서드가 스레드 ID에 액세스하는 데 사용되었습니다. 이 식별자는 Linux® 운영 체제의 최상위 명령에 의해 출력되는 식별자와 다르며, 이 체계는 OpenMP가 스레드 수를 추적하는 방법일 뿐입니다. C++ 코드에서 firstprivate 지시문을 사용할 계획이라면 또 다른 기능에 유의하세요. firstprivate 지시문에서 사용하는 변수는 메인 스레드 변수에서 자신을 초기화하는 복사 생성자입니다. 따라서 복사 생성자가 클래스에 대해 비공개인 경우에는 다음과 같은 일이 발생할 수 있습니다. 불쾌한 결과를 초래합니다. 이제 여러 면에서 동전의 반대편인 lastprivate 지시어로 넘어가겠습니다.

lastprivate 지시어

스레드 로컬 변수를 메인 스레드의 데이터와 동기화하는 대신, 메인 스레드의 변수를 마지막 루프의 결과로 얻을 데이터와 동기화합니다. 목록 16에서는 병렬 for 루프를 실행합니다.

목록 16. 기본 스레드와의 데이터 동기화가 없는 병렬 for 루프
#포함하다 #포함하다 int main() ( int idx = 100; int main_var = 2120; #pragma omp 병렬 for private(idx) for (idx = 0; idx< 12; ++idx) { main_var = idx * idx; printf("В потоке %d idx = %d main_var = %d\n", omp_get_thread_num(), idx, main_var); } printf("Возврат в главный поток со значением переменной main_var = %d\n", main_var); }

내 8코어 컴퓨터에서 OpenMP는 블록용 병렬 스레드를 6개 생성합니다. 각 스레드는 차례로 루프에서 두 번의 반복을 갖습니다. main_var 변수의 최종 값은 마지막으로 실행된 스레드에 따라 달라지므로 해당 스레드의 idx 변수 값에 따라 달라집니다. 즉, main_var 변수의 값은 idx 변수의 마지막 값에 종속되지 않고, 마지막으로 실행된 스레드에 포함된 idx 변수의 값에 따라 달라집니다. 이 예제는 목록 17에 설명되어 있습니다.

목록 17. 마지막으로 실행된 스레드에서 main_var 변수 값의 종속성
스레드 4에서 idx = 8 main_var = 64 스레드 2에서 idx = 4 main_var = 16 스레드 5에서 idx = 10 main_var = 100 스레드 3에서 idx = 6 main_var = 36 스레드 0에서 idx = 0 main_var = 0 스레드 1에서 idx = 2 main_var = 4 스레드 4 idx = 9 main_var = 81 스레드 2 idx = 5 main_var = 25 스레드 5 idx = 11 main_var = 121 스레드 3 idx = 7 main_var = 49 스레드 0 idx = 1 main_var = 1 In thread 1 idx = 3 main_var = 9 변수 값 main_var = 9를 사용하여 메인 스레드로 돌아갑니다.

목록 17의 코드를 여러 번 실행하여 기본 스레드의 main_var 값이 항상 실행된 마지막 스레드의 idx 값에 따라 달라지는지 확인하세요. 루프에서 메인 스레드 변수의 값을 idx 변수의 최종 값과 동기화해야 한다면 어떻게 될까요? Listing 18에 설명된 것처럼 lastprivate 지시문이 유용한 곳입니다. 이전 예제와 마찬가지로 Listing 18의 코드를 몇 번 실행하면 메인 스레드에 있는 main_var 변수의 최종 값이 121임을 알 수 있습니다. (즉, 루프의 마지막 반복에서 idx 변수의 값)

목록 18. lastprivate 지시문을 사용한 동기화
#포함하다 #포함하다 int main() ( int idx = 100; int main_var = 2120; #pragma omp 병렬 for private(idx) lastprivate(main_var) for (idx = 0; idx< 12; ++idx) { main_var = idx * idx; printf("В потоке %d idx = %d main_var = %d\n", omp_get_thread_num(), idx, main_var); } printf("Возврат в главный поток со значением переменной main_var = %d\n", main_var); }

목록 19는 목록 18의 코드를 실행한 결과를 보여줍니다.

목록 19. 목록 18의 코드를 실행한 결과(메인 스레드에서 main_var은 항상 121로 설정됨)
스레드 3에서 idx = 6 main_var = 36 스레드 2에서 idx = 4 main_var = 16 스레드 1에서 idx = 2 main_var = 4 스레드 4에서 idx = 8 main_var = 64 스레드 5에서 idx = 10 main_var = 100 스레드 3에서 idx = 7 main_var = 49 스레드 0에서 idx = 0 main_var = 0 스레드 2에서 idx = 5 main_var = 25 스레드 1에서 idx = 3 main_var = 9 스레드 4에서 idx = 9 main_var = 81 스레드 5에서 idx = 11 main_var = 121 V thread 0 idx = 1 main_var = 1 변수 값 main_var = 121을 사용하여 메인 스레드로 돌아갑니다.

마지막 참고 사항: C++ 개체에서 lastprivate 연산자를 지원하려면 해당 클래스에 사용 가능한 public 연산자= 메서드가 있어야 합니다.

OpenMP의 병합 정렬

OpenMP가 작업 실행 시간을 단축하는 실제 사례를 살펴보겠습니다. OpenMP 사용의 이점을 충분히 보여주기에 충분할 정도로 최적화되지 않은 병합 정렬 절차 버전을 살펴보겠습니다. 이 예제는 목록 20에 표시됩니다.

목록 20. OpenMP의 병합 정렬
#포함하다 #포함하다 #포함하다 네임스페이스 std 사용; 벡터 병합(const 벡터 & 왼쪽, const 벡터 & 오른쪽) (벡터 결과; 부호 없는 left_it = 0, right_it = 0; 동안(left_it< left.size() && right_it < right.size()) { if(left < right) { result.push_back(left); left_it++; } else { result.push_back(right); right_it++; } } // Занесение оставшихся данных из обоих векторов в результирующий while(left_it < left.size()) { result.push_back(left); left_it++; } while(right_it < right.size()) { result.push_back(right); right_it++; } return result; } vector병합정렬(벡터 & vec, int 스레드) ( // 종료 조건: 목록에 요소가 하나만 포함된 경우 // 목록이 완전히 정렬됩니다. if(vec.size() == 1) ( return vec; ) // 중간 위치를 결정합니다. 표준 벡터의 요소::벡터 ::반복자 middle = vec.begin() + (vec.size() / 2); 벡터 left(vec.begin(), middle); 벡터 오른쪽(가운데, vec.end()); // 두 개의 더 작은 벡터에 대해 병합 정렬을 수행합니다. if (threads > 1) ( #pragma omp 병렬 섹션 ( #pragma omp 섹션 ( left = mergesort(left, thread/2); ) #pragma omp section ( right = mergesort(right, 스레드 - 스레드/2); ) ) ) else ( left = mergesort(left, 1); right = mergesort(right, 1); ) return merge(left, right); ) int main() ( 벡터 v(1000000); for (long i=0; i<1000000; ++i) v[i] = (i * i) % 1000000; v = mergesort(v, 1); for (long i=0; i<1000000; ++i) cout << v[i] << "\n"; }

8개의 스레드를 사용하여 병합 정렬 절차를 실행하면 실행 시간이 3.7초(단일 스레드 사용)에서 2.1초로 단축되었습니다. 여기서는 스레드 수에 주의하면 됩니다. 저는 8개의 스레드로 시작했지만, 이를 사용하여 얻을 수 있는 실제 이점은 시스템 구성에 따라 달라질 수 있습니다. 스레드 수를 명시적으로 제한하지 않으면 수천 개가 아니더라도 수백 개의 스레드가 생성될 수 있으며 이로 인해 시스템 성능이 저하될 가능성이 매우 높습니다. 또한 병합 정렬 코드로 작업할 때 앞서 설명한 pragma 섹션을 사용하는 것이 유용합니다.

결론

여기서 기사를 마치겠습니다. 병렬 프라그마와 스레드를 광범위하게 생성하는 방법을 다루었고, OpenMP가 작업 실행 시간을 줄이고 동기화와 유연한 제어를 허용하는 방법을 살펴봤으며 병합 정렬을 사용한 실제 예를 살펴보았습니다. 물론, 아직 배워야 할 것이 많으며, 이를 배울 수 있는 가장 좋은 곳은 OpenMP 프로젝트 웹 사이트입니다. 모든 추가 정보는 섹션에서 확인할 수 있습니다.

OpenMP는 병렬 공유 메모리 시스템을 위한 애플리케이션 프로그래밍 인터페이스 표준입니다. C, C++, Fortran 언어를 지원합니다.

OpenMP 프로그램 모델

병렬 프로그램 모델 오픈MP다음과 같이 공식화될 수 있다:

  • 프로그램은 직렬 및 병렬 섹션으로 구성됩니다(그림 2.1).
  • 초기에는 프로그램의 연속적인 섹션을 실행하는 메인 스레드가 생성됩니다.
  • 평행구간 진입시 연산 수행 포크, 스레드 계열을 생성합니다. 각 스레드에는 고유한 숫자 식별자가 있습니다(메인 스레드는 0입니다). 루프가 병렬화되면 모든 병렬 스레드는 동일한 코드를 실행합니다. 일반적으로 스레드는 다양한 코드 조각을 실행할 수 있습니다.
  • 병렬구간을 빠져나오면 해당 작업이 수행됩니다. 가입하다.메인 스레드를 제외한 모든 스레드의 실행이 종료됩니다.

오픈MP다음 구성 요소로 구성됩니다.

  • 컴파일러 지시어- 스레드를 생성하고 스레드 간에 작업을 배포하고 동기화하는 데 사용됩니다. 지시문은 프로그램의 소스 코드에 포함되어 있습니다.
  • 런타임 라이브러리 루틴- 스트림 속성을 설정하고 정의하는 데 사용됩니다. 이러한 루틴에 대한 호출은 프로그램의 소스 코드에 포함되어 있습니다.
  • 환경 변수- 병렬 프로그램의 동작을 제어하는 ​​데 사용됩니다. 환경 변수는 적절한 명령(예: UNIX 운영 체제의 셸 명령)을 통해 병렬 프로그램의 실행 환경에 대해 설정됩니다.

컴파일러 지시문과 런타임 라이브러리 루틴의 사용에는 프로그래밍 언어에 따라 달라지는 규칙이 적용됩니다. 그러한 규칙의 집합을 다음과 같이 부릅니다. 언어에 국한된.

포트란 언어 바인딩

포트란 프로그램에서 컴파일러 지시문, 서브루틴 및 환경 변수의 이름은 OMP로 시작됩니다. 체재 컴파일러 지시문:

(!|C|*)$OMP 지시어 [operator_1[, Operator_2, ...]]

지시문은 첫 번째(고정 Fortran 77 텍스트 형식) 또는 임의(자유 형식) 줄 위치에서 시작됩니다. 다음 줄에서 지시문을 계속하는 것이 가능합니다. 이 경우 연속 줄을 표시하기 위한 이 언어 버전의 표준 규칙이 적용됩니다(고정 레코드 형식의 경우 6번째 위치에 있는 공백이 아닌 문자 및 레코드 형식의 경우 앰퍼샌드). 무료 형식).

OpenMP를 사용하는 Fortran 프로그램 예제

프로그램 omp_example 정수 i, k, N real*4 sum, h, x print *, "N을 입력하세요:" read *, N h = 1.0 / N sum = 0.0 C$OMP PARALLEL DO SCHEDULE(STATIC) REDUCTION( +:sum) do i = 1, N x = i * h sum = sum + 1.e0 * h / (1.e0 + x**2) enddo print *, 4.0 * sum end