<< MySQL innodb_flush_method 튜닝 포인트 | Home | MySQL++과 OpenMP를 활용한 MySQL Benchmark 검토 >>

OpenMP를 이용한 병렬 프로그래밍

개요

OpenMP는 여러개의 프로세스가 공유된 메모리를 참조하는 환경에서 다중 스레드 병렬 프로그래밍을 위한 표준 스펙이다. 여기에서 제공하는 API를 통해 사용자들은 어플리케이션의 성능 향상을 얻을 수 있게 된다.



OpenMP의 주요 동기요인은 성능, 확장성, 이식성, 표준성을 목표로 한다. 그리고 C, C++(#으로 시작), Fortran(90에서는 !$로 시작)을 지원하고 있다.
역사로는 1997년에 Fortran 1.0가 최초로 나왔고 그 후 1998년 C/C++ 1.0이 출시, 2005년에 OpenMP 2.5가, 2008년에 OpenMP 3가 나와서 현재에 이르고 있다.
OpenMP를 주로 사용하는 방법은 프로파일링을 통해 시간을 많이 차지하는 부분을 식별하고 해당 프로그램에서 사용되는 의존성, 데이터의 유효 범위를 예측한 다음 지시어의 설정을 통해 병렬 처리를 하여 성능 향상을 얻는 방식으로 진행된다.

구성



1) Directives(컴파일러 지시자)
쓰레드 사이의 작업 분담, 통신, 동기화를 담당을 한다. 컴파일러가 지시자를 참고하여 다중 쓰레드 코드를 생성하게 된다.
Work-Sharing 지시자는 상세히 설명할 필요가 있어서 아래에 새로운 단락으로 빼서 설명하고 여기서는 동기화, 데이터 유효 범위 지시자를 설명한다. OpenMP 프로그래밍은 컴파일러가 모두 알아서 처리하는게 아니라 동기화 및 의존성 제거 등의 작업은 사용자가 직접 소스코드에 기술해 주어야 한다는 점에서 병렬프로그래밍이 어렵게 인식되는 부분중에 하나다.

- 상호 배제 동기화
  • critical : 병렬영역 안에서 critical section을 지정하면 그 영역은 하나의 스레드에서 실행 된다.
  • atomic : 하나의 스칼라 변수를 경신하는 지정된 단일 문장(single statement)에 대해 여러 스레드가 접근하는 것을 방지한다. mini-critical section의 역할과 비슷하다.
  • flush : 하나의 메모리에 여러 개의 스레드가 경합을 벌이는 경우 그 정합성을 유지시켜 주는 지시어
- 이벤트 동기화
  • barrier : 모든 스레드들이 barrier에 도달할 때까지 대기하게 된다.
  • ordered : ordered section 내부의 루프 실행을 순차적으로 진행한다.
  • master : master section을 마스터 쓰레드에서만 실행시킨다.
  • taskwait : 모든 task가 taskwait에 도달할 때까지 대기한다.
- 데이터의 유효 범위 지시자
  • private : 지정된 변수를 스레드끼리 공유하는 것 방지한다. 주로 스칼라값, 여러 쓰레드들이 동시에 접근해서 쓰기를 할경우 사용.
  • shared : 지정된 변수를 모든 스레드가 공유하도록 한다. 디폴트.
  • default : private 또는 shared로 선언되지 않은 변수의 기본적인 유효범위 지정한다.
  • firstprivate : private 변수처럼 각 쓰레드에 개별적으로 변수 생성하고 각 스레드 마다 순차 영역에서 가져온 값으로 초기화한다.
  • lastprivate : private 변수처럼 각 스레드에 개별적으로 변수 생성하고 순차 실행에서 마지막 계산에 해당되는 값. 즉, 마지막 반복실행의 값을 마스터 스레드에게 넘겨준다.
  • reduction : reduction 변수는 shared이고 다중 스레드에서 병렬로 수행된 계산 결과를 환산해 최종 결과를 마스터 스레드로 내 놓는다.
  • schedule : 작업의 균등 분배를 위해 schedule 을 사용한다.
2) Runtime Library
병렬화에 직접 참여는 하지 않지만 실행 환경에서 병렬 매개 변수(참여 스레드의 개수, 번호 등)의 설정과 조회를 통해 병렬화 정보를 변경 조회할 수 있다.
  • omp_set_num_threads : 병렬 영역에서 사용할 스레드 개수 설정
  • omp_get_num_threads() : 병렬 영역 안에서 호출되어 생성된 스레드의 개수를 리턴
  • omp_get_thread_num() : 병렬 영역 안에서 생성된 스레드들의 ID를 리턴
  • omp_get_max_threads() : 병렬 영역에서 사용 가능한 최대 스레드 개수 리턴
  • omp_set_nested() : nested 병렬성 지원 여부 결정
  • omp_set_dynamic() : 순차 영역에서 호출되어 이어지는 병렬 영역 들의 스레드 개수 할당을 동적으로 수행
  • omp_get_nested() : 호출되는 시점의 nested 병렬성 설정 여부 확인
  • omp_get_dynamic() : 스레드 할당 방식이 동적인지 확인
  • omp_in_parallel() : 호출된 지점이 순차 영역인지 병렬 영역인지 확인
  • omp_get_num_procs() : 프로그램에서 사용 가능한 물리적 프로세서의 총 개수 확인
3) Environment Variables
Runtime Library와 중복되는 부분이 많지만 우선순위는 동일한 지시자를 반복 사용했을 경우 Runtime Library가 우선된다. 주요한 일은 실행 시스템의 병렬 매개 변수(스레드 개수 등)를 설정해 실행시에 제어를 한다.
  • OMP_NUM_THREADS : 병렬영역에서 사용 가능한 최대 스레드 개수 지정
  • OMP_SCHEDULE : schedule type이 runtime으로 지정된 루프들에게 scheduling 방식 지정
  • OMP_DYNAMIC : 스레드 개수의 동적할당 여부 결정
  • OMP_NESTED : nested 병렬성 지원 여부 결정


프로그래밍 모델

1) Fork-Join 모델
쓰레드가 병렬 구문를 만나면 쓰레드는 그 자신을 포함한 추가적인 쓰레드(0개 이상의)로 이루어진 쓰레드 팀을 만든다(Fork). 병렬 구문를 만난 쓰레드는 팀의 마스터 쓰레드를 호출 하게 되고(Join) 다른 쓰레드들은 팀의 slave 쓰레드라고 불리어진다.



Work-Sharing 모델

같은 작업을 쓰레드별로 실행하는 것이 아니라 작업을 분할해서 쓰레드별로 나누어서 실행하는 것을 말한다.

1) for
바로 뒤에 오는 for 루프의 반복 실행을 쓰레드에 분배한다. 그리고 루프 끝에 암시적 장벽(동기화)이 존재한다. 이를 피할려면 nowait 사용하면 된다.
데이터 병렬화시에 활용된다.



#include <stdio.h>
#include <omp.h>

int main(int argc, char *argv[]) {
  int i;
#pragma omp parallel for
  for(i=0; i<10; i++) {
    printf("[%d-%d] Hello World\n", omp_get_thread_num(), i);
  }
   return 0;
}
> gcc -fopenmp -o for_hello for_hello.c
> ./for_hello
[0-0] Hello World
[0-1] Hello World
[0-2] Hello World
[2-6] Hello World
[2-7] Hello World
[2-8] Hello World
[3-9] Hello World
[1-3] Hello World
[1-4] Hello World
[1-5] Hello World

2) sections
독립된 여러 개 작업을 각 스레드에 분산 실행한다. 그리고 sections 구문 끝에 암시적 장벽(동기화)이 존재하고 이를 피할려면 nowait 사용하면 된다.
기능적 분할에 이용된다.



#include <stdio.h>
#include <omp.h>

int main(int argc, char *argv[]) {
#pragma omp parallel
#pragma omp sections
  {
#pragma omp section
    {
      printf("[%d] Hello \n", omp_get_thread_num());
    }
#pragma omp section
    {
      printf("[%d] World \n", omp_get_thread_num());
    }
  }
  return 0;
}
> gcc -fopenmp -o section_hello section_hello.c
> ./section_hello
[0] Hello 
[3] World 

3) single
병렬 영역 내부에서 하나의 스레드만을 이용해 작업 수행한다. single 지시어자 가장 먼저 접근한 스레드에 작업 할당된다. single 구문 끝에 암시적 장벽(동기화)이 존재한다.마찬가지로 이를 피하기 위해서는 nowait 사용하면 된다.
병렬 영역 내에서의 입/출력 수행에 주로 사용된다.



#include <stdio.h>
#include <omp.h>

int main(int argc, char *argv[]) {
#pragma omp parallel
 {
#pragma omp single
    {
      printf("[%d] Hello \n", omp_get_thread_num());
      
    }
    printf("[%d] World \n", omp_get_thread_num());
  }
  return 0;
}

> gcc -fopenmp -o single_hello single_hello.c
> ./single_hello
[3] Hello 
[3] World 
[2] World 
[1] World 
[0] World 

4) Task
OpenMP 버전 3.0 이상부터 추가된 지시자며, 수행할 작업을 한번의 호출로 진행되는 작업이나 한번의 구문으로 실행 가능한 작업 단위 나눈다. 태스트 실행은 작업 스케쥴링이 적용된다.
#include <stdio.h>
#include <omp.h>

int main(int argc, char *argv[]) {
#pragma omp parallel
  {
#pragma omp single
    {
#pragma omp task
      {
        printf("Hello ");
      }
#pragma omp task
      {
        printf("World ");
      }
#pragma omp taskwait
      printf("\nThanks! ");
    }
  }
  printf("\n");
  return 0;
}

> gcc -fopenmp -o task_hello task_hello.c
> ./task_hello 
Hello World 
Thanks! 

추후에는 mysql++ 라이브러리를 활용해서 MySQL Benchmark 클라이언트 성능 개선 예제를 올려볼까 합니다.

[참조 사이트]
Tags : ,


Re: OpenMP를 이용한 병렬 프로그래밍

 예제랑 쉽게 설명해줘서 쉽게 이해가 되네요. 물론 더 나아가면서 발견하게 되는 난관들은 분명 존재하겠지만요..


Add a comment Send a TrackBack