Skip to main content

멀티쓰레딩을 편리하게 해주는 OpenMP 사용법

·7 mins· loading · loading ·
CS Multicore-GPU-Programming
Soeun Uhm
Author
Soeun Uhm
problem-solving engineer, talented in grit.
Table of Contents
Multicore-GPU-Programming - This article is part of a series.
Part 5: This Article

OpenMP - why?
#

지금까지 개별적인 쓰레드를 이용한 병렬프로그래밍 방법들을 봤습니다. OpenMP 는 코드에 따른 적절한 수의 쓰레드를 만들고, 스케쥴 해주어 코드를 자동으로 병렬화 시켜줍니다.

carbon

OpenMP 는 실제 어셈블리 명령어를 만드는 것이 아니라, 컴파일러에게 정보를 주는 방식으로 작동합니다. 병렬화가 쉽고, 쉬운 문법을 가지고 있지만 컴파일러가 어떠한 작업을 할지 확실하지 않을 때는 원하든 결과가 나오지 않을 수 도 있습니다.

Directives
#

#pragma omp parallel {...} 은 프로그래머와 컴파일러 사이의 인터페이스 처럼 작동합니다. 이것을 사용해 병렬화가 필요한 지역을 지정할 수 있습니다. {} 내에 있는 지역은 fork-join 을 사용하여 병렬화됩니다.

image

Critical section
#

임계구역은 보통 아래 처럼 설정할 수 있습니다.

m.lock();
// critical section...
m.unlock();

OpenMp 를 사용하면, 아래와 같이 설정할 수 있습니다.

#pragma omp critical
{
// critical section...
}

그럼 병렬처리와 임계 구역을 동시에 설정해야 할때는 어떻게 해야할까요?

a();
#pragma omp parallel
{
	c(1);
	#pragma omp critical
	{
		c(2); //critical section
	}
	c(3);
	c(4);
}
z();

image

Shared/private
#

Shared
#

std::threads 에서는 함수 안에 있는 변수는 private 이었습니다. OpenMP 에선, shared 가 기본값입니다.

#include <omp.h>
#include <cstdio>
#include <cassert>
#include <stdlib.h>

int main(int argc, char **argv)
{
	int shared_int = -1;
	omp_set_num_threads(2);
	#pragma omp parallel
	{
		//shared_int in shared
		int tid = omp_get_thread_num();
		printf("Thread ID %2d  | shared_int = %d\n",tid,shared_int);
	}
...
}

만약 shared_int 변수를 수정하면, race condition 이 생깁니다.

Private
#

#include <cassert>
#include <cstdio>
#include <omp.h>
#include <stdlib.h>

int main(int argc, char **argv)
{
    omp_set_num_threads(2);
    int is_private = -2;

/*
private : not initialized
Modifying is_private within parallel block does not modify the value outside block
*/
#pragma omp parallel private(is_private)
    {
        int tid = omp_get_thread_num();
        printf("Thread ID %2d  | is_private(before) = %d\n", tid, is_private);
        is_private = tid;
        printf("Thread ID %2d  | is_private(after) = %d\n", tid, is_private);
        assert(is_private == tid);
    }
    printf("Main thread  | is_private = %d\n", is_private);

    return 0;
}
image

is_private 변수가 각 쓰레드마다 할당되었음을 볼 수 있습니다. 하지만 똑같이 초기화 된 것은 아닙니다. {} 안에서 is_private 변수를 수정해도 블럭 바깥에는 영향을 미치지 않습니다.

First private
#

Firstprivate 를 사용하면 모든 로컬 변수가 초기화됩니다.

#include <cassert>
#include <cstdio>
#include <omp.h>
#include <stdlib.h>

int main(int argc, char **argv)
{
    omp_set_num_threads(2);
    int is_private = -2;
/*
firstprivate : initialized to value outside region
Modifying is_private within parallel block does not modify the value outside the block
*/
#pragma omp parallel firstprivate(is_private)
    {
        int tid = omp_get_thread_num();
        printf("Thread ID %2d  | is_private(before) = %d\n", tid, is_private);
        is_private = tid;
        printf("Thread ID %2d  | is_private(after) = %d\n", tid, is_private);
        assert(is_private == tid);
    }
    printf("Main thread  | is_private = %d\n", is_private);

    return 0;
}
image

범위 바깥에 is_private 변수를 -2로 초기화한것이 그래도 적용됨을 볼 수 있습니다. 그러나 여전히 블럭 내에서 is_private 변수를 수정하는 것은 블럭 바깥에 영향을 미치지 않습니다.

Last private
#

#include <cassert>
#include <cstdio>
#include <omp.h>
#include <stdlib.h>

int main(int argc, char **argv)
{
    omp_set_num_threads(2);
    int last_private = -2;
/*
lastprivate : not initialized,
becomes what's written in the last iteration (no matter when, which thread executed it!)
*/
#pragma omp parallel for lastprivate(last_private)
    for (int i = 0; i < 10; i++)
    {
        int tid = omp_get_thread_num();
        printf("Thread ID %2d excuting i=%d  | last_private(before) = %d\n", tid, i, last_private);
        last_private = i;
        printf("Thread ID %2d excuting i=%d  | last_private(after) = %d\n", tid, i, last_private);
        assert(last_private == i);
    }
    printf("Main thread  | last_private = %d\n", last_private);

    return 0;
}

Lastprivate 는 이터레이션 내 마지막으로 수정된 값으로 고정됩니다. 그리고 private 과 같이 초기화되지 않습니다.

image

FirstPrivate & LastPrivate
#

#include <cassert>
#include <cstdio>
#include <omp.h>
#include <stdlib.h>

int main(int argc, char **argv)
{
    omp_set_num_threads(2);
    int last_private = -2;
/*
lastprivate & firstprivate : initialized,
becomes what's written in the last iteration (no matter when, which thread executed it!)
*/
#pragma omp parallel for firstprivate(last_private) lastprivate(last_private)
    for (int i = 0; i < 10; i++)
    {
        int tid = omp_get_thread_num();
        printf("Thread ID %2d excuting i=%d  | last_private(before) = %d\n", tid, i, last_private);
        last_private = i;
        printf("Thread ID %2d excuting i=%d  | last_private(after) = %d\n", tid, i, last_private);
        assert(last_private == i);
    }
    printf("Main thread  | last_private = %d\n", last_private);

    return 0;
}

firstpriavate, lastprivate 같이 사용할 수 도 있습니다.

image

Sections
#

병렬화를 위한 다른 sections 를 제공할 수 도 있습니다.

#include <cassert>
#include <cstdio>
#include <omp.h>
#include <stdlib.h>

int main(int argc, char **argv)
{
    omp_set_num_threads(10);

#pragma omp parallel sections
    {
#pragma omp section
        for (int i = 0; i < 10; i++)
        {
            int tid = omp_get_thread_num();
            printf("Thread ID %2d section A\n", tid);
        }

#pragma omp section
        for (int i = 0; i < 10; i++)
        {
            int tid = omp_get_thread_num();
            printf("Thread ID %2d section B\n", tid);
        }
    }

    return 0;
}
image

Single
#

만약 하나의 블럭이 하나의 쓰레드로만 실행하려고 하면, single 을 사용할 수 있습니다.

image

#include <cassert>
#include <cstdio>
#include <omp.h>
#include <stdlib.h>

int main(int argc, char **argv)
{
    omp_set_num_threads(10);

#pragma omp parallel
    {
#pragma omp single
        for (int i = 0; i < 10; i++)
        {
            int tid = omp_get_thread_num();
            printf("Thread ID %2d section A\n", tid);
        } // implicit barrier !!

#pragma omp for
        for (int i = 0; i < 100; i++)
        {
            int tid = omp_get_thread_num();
            printf("Thread ID %2d section B\n", tid);
        }
    }

    return 0;
}
image

Barriers
#

OpenMP 는 블럭 끝에 implicit barrier 를 가지고 있습니다.

a();
#pragma omp parallel 
{
	b();
	#pragma omp for
	for (int i=0;i<10;++i){
		c(i);
	}
	d();
}
z();

image

만약 implicit barrier 를 원하지 않으면, nowait 을 사용하면 됩니다.

a();
#pragma omp parallel 
{
	b();
	#pragma omp for nowait
	for (int i=0;i<10;++i){
		c(i);
	}
	d();
}
z();

image

만약 명시적으로 barrier 를 사용하고 싶다면, #pragma omp barrier 를 설정하면 됩니다.

Master
#

section 과 비슷하지만, master 쓰레드에 의해 실행되는 것을 보장합니다.

#pragma omp parallel 
{
	#pragma omp master
	for(int i=0;i<100;i++){
		int tid = omp_get_thread_num();
		printf("Thread ID %2d section A\n",tid);
	}

	#pragma omp for
	for(int i=0;i<100;i++){
		int tid = omp_get_thread_num();
		printf("Thread ID %2d section B\n",tid);
	}
}

이렇게 하면 Thread ID 가 0인 master 쓰레드만 section A를 실행합니다.

image

Master + explicit barrier 를 같이 쓰면, single 과 비슷하게 작동합니다.

#include <cassert>
#include <cstdio>
#include <omp.h>
#include <stdlib.h>

int main(int argc, char **argv)
{
    omp_set_num_threads(10);

#pragma omp parallel
    {
/*
master + explicit barrier works like "single"
*/
#pragma omp master
        for (int i = 0; i < 5; i++)
        {
            int tid = omp_get_thread_num();
            printf("Thread ID %2d section A\n", tid);
        }
#pragma omp barrier
#pragma omp fpr
        for (int i = 0; i < 5; i++)
        {
            int tid = omp_get_thread_num();
            printf("Thread ID %2d section B\n", tid);
        }
    }

    return 0;
}
image

image

Reduction
#

int main(int argc, char **argv)
{
	int sum=0;
	#pragma omp parallel for
	for (int i=0;i<100;i++) {
		sum += i;
	}
	printf("Sum : %d\n", sum);
	return 0;
}
image

이 경우에, 답은 엉뚱한 값이 나옵니다. 어떻게 하면 정확한 결과를 얻을까요?

#include <iostream>
#include <omp.h>

int main(int argc, char **argv)
{
    omp_set_num_threads(10);
    int sum = 0;

#pragma omp parallel for reduction(+ : sum)
    for (int i = 0; i < 100; i++)
    {
        sum += i;
    }
    printf("Sum : %d\n", sum);
    return 0;
}

reduction 을 사용하면 local sum 을 만들고, global sum에 더해줍니다. 0부터 99까지 합인 4950이 올바르게 출력되는 것을 볼 수 있습니다.

image

Nested loop
#

omp_set_num_threads(4);
a();
#pragma omp parallel for
for (int i=0;i<4;i++) {
	for (int j=0;j<4;j++) {
		c(i,j);
	}
}
z();

parallel for 는 가장 바깥쪽 루프만 병렬화합니다.

image

omp_set_num_threads(4);
a();
#pragma omp parallel for
for (int i=0;i<3;i++) {
	for (int j=0;j<6;j++) {
		c(i,j);
	}
}
z();

만약 가장 바깥 쪽 루프의 이터레이션 수보다 쓰레드의 수가 많으면 어떻게 될까요? 아래 처럼 쓰레드 한개가 idle 한 상황이 발생합니다.

image

이 상황을 해결하는 방법들을 봅시다.

Bad Way(1)

omp_set_num_threads(4);
a();
for (int i=0;i<3;i++) {
	#pragma omp parallel for
	for (int j=0;j<6;j++) {
		c(i,j);
	}
}
z();

안쪽 루프를 병렬화하는 방법입니다. 가끔 해결책이 될 수 있지만, 항상 해결책이 되지는 않습니다.

image

Bad Way(2)

omp_set_num_threads(4);
a();
#pragma omp parallel for
for (int i=0;i<3;i++) {
	#pragma omp parallel for
	for (int j=0;j<6;j++) {
		c(i,j);
	}
}
z();

이렇게 안쪽, 바깥쪽 루프에 모드 병렬화를 하면 두번째 pragma 는 무시됩니다. 따라서 맨 위에만 pragma 를 붙인 것과 같은 결과입니다.

Good way(1)

a();
#pragma omp parallel for
for (int ij = 0; ij < 3; ++ij) {
	c(ij/6, ij%6);
}
z();

중첩된 루프를 하나의 루프로 푸는 방식입니다. 따라서 총 18번의 이터레이션이 생깁니다.

image

Good way(2)

위의 방식을 자동으로 해주는 pragma 의 collapse 를 사용할 수 있습니다.

omp_set_num_threads(4);
a();
#pragma omp parallel for collapse(2)
for (int i=0;i<3;i++) {
	for (int j=0;j<6;j++) {
		c(i,j);
	}
}
z();

Summary
#

OpenMP 를 사용하면, 쓰레드를 직접 만들었을 때보다 간편하게 병렬 프로그래밍을 할 수 있지만, 어디서 틀렸는지 정확히 모를 수 도 있다는 단점이 있습니다. 따라서 정확하게 문법을 숙지하여 openmp 를 사용해야 합니다.

작성한 실제 코드는 아래 레포지토리의 openmp 폴더에서 볼 수 있습니다.

ddoddii/Multicore-GPU-Programming

[CSI4119] Multicore GPU Programming

C++
0
0

Reference
#

Multicore-GPU-Programming - This article is part of a series.
Part 5: This Article

Related

멀티쓰레드에서 행렬 연산(matmul) 성능 증가시키는 방법들
·7 mins· loading · loading
CS Multicore-GPU-Programming
캐시구조에 따른 행렬곱 연산 성능 높이기
멀티쓰레드에서 쓰레드 간 작업을 어떻게 균일하게 분할할까?
·4 mins· loading · loading
CS Multicore-GPU-Programming
workload balancing, thread pool
c++로 알아본 쓰레드 프로그래밍
·9 mins· loading · loading
CS Multicore-GPU-Programming
쓰레드 관리하기, thread-safety 구현하는 방법