2010年2月9日火曜日

CでOpenMPを使ってみる

せっかくマルチコアCPUがあることだし、全コアを全力で使えるプログラムを作ってみたいと考えていたが、いろいろ面倒で諦めがちだった。しかし、C/C++にはOpenMPという便利なものがあると聞いて、調べみたところ、本当に便利だったのでここに纏めてみる。


OpenMPを利用するには、GCC 4.2以降が必要で、コンパイル時に-fopenmpオプションを付ける必要がある。
% gcc openmp_test.c -o openmp_test -fopenmp

また、今回OpenMP関連で使用した関数とその役割は以下のとおり。
omp_get_thread_num() … 現在実行されているスレッドの番号を返す
omp_set_num_threads(10) … スレッド数を10に指定(通常はCPUコア数から自動的に割り振られる)


まずは一番簡単なスレッド作成方法。
スレッド処理したい関数の前に#pragma omp parallelを書くだけ。

#include<stdio.h>

int main(void){
int i;

#pragma omp parallel
for(i=0; i<20; i++){
printf("Thread %d : %d\n", omp_get_thread_num(), i);
}
return 0;
}


これをコンパイルし実行すると、

miku@kagamine:~/c-lang$ ./openmp_1
Thread 0 : 0
Thread 0 : 1
Thread 0 : 2
Thread 0 : 3
Thread 0 : 4
Thread 1 : 0
Thread 1 : 1
Thread 1 : 2
Thread 1 : 3
Thread 1 : 4

となったり、

miku@kagamine:~/c-lang$ ./openmp_1
Thread 1 : 0
Thread 1 : 1
Thread 1 : 2
Thread 1 : 3
Thread 1 : 4
Thread 0 : 0

となったりする。なにやら同時に処理されているように見え、スレッドの実験としては成功していることが分かる。
しかし、結果が安定していないため、このままではあまり使い物にならない。
開始したスレッド全てから変数iが参照され、変更されるためこうなってしまう。



そこで、変数iをスレッド内でプライベートに使うようにしてみる。

#include<stdio.h>

int main(void){
int i;

// 変数iをスレッド内プライベートとして渡し、スレッドを開始させる
#pragma omp parallel private(i)
for(i=0; i<3; i++){
printf("Thread %d : %d\n", omp_get_thread_num(), i);
}

return 0;
}

これを実行すると…

miku@kagamine:~/c-lang$ ./openmp_2
Thread 0 : 0
Thread 1 : 0
Thread 1 : 1
Thread 1 : 2
Thread 0 : 1
Thread 0 : 2

iの値はそのスレッド内のみの値として扱われ、
スレッド毎にi++されていることが分かる。



通常、全スレッドが全く同じ処理を繰り返しても意味が無い。
そこで、グローバルな変数iを操作するときのみ、排他処理を行わせる。

#include<stdio.h>

// 変数iをグローバルとして渡すが、printfとi++の部分を排他動作させる
int main(void){
int i;

i = 0;
#pragma omp parallel
while (i<10){
// 排他処理部分をブロックで指定
#pragma omp critical
{
printf("Thread %d : %d\n", omp_get_thread_num(), i);
i++;
}

sleep(1);
}
return;
}

これを実行すると…

miku@kagamine:~/c-lang$ ./openmp_3
Thread 1 : 0
Thread 0 : 1
Thread 1 : 2
Thread 0 : 3
Thread 1 : 4
Thread 0 : 5
Thread 1 : 6
Thread 0 : 7
Thread 1 : 8
Thread 0 : 9

iの値が、両スレッドで1づつ増えていっているのが分かる。

ただ、このテストプログラムには問題がある。
while (i<0)の判定後、whileループの中で別スレッドがiに値を追加し、
printf()するときにiの値が10または10以上になってしまうことがある(sleep(1)を外すと頻繁に発生する)。
この辺りを、正しく処理させるようにしないと、「数回に1回だけ何故かコケる」なんて厄介なバグになってしまうので、注意したい。



何でもかんでも排他処理させてしまうと、他の関数で実行中のスレッドにまで影響が及び、何かしらの問題(デッドロック等)が発生する可能性がある。
そこで、できる限り排他処理を使わないようにしてみる。
(ついでに、しっかり4コア使ってくれるのか確かめるため、dummy関数を用意した)

#include<stdio.h>
#include<unistd.h> // usleep()
#include<stdlib.h>


// ダミー処理関数(CPUに負荷を掛ける)
void dummy(int number){
int n, i, tmp;
int array[number];
for(i=0; i<number; i++){
array[i] = i;
}

for (n=0; n<number; n++){
for (i=0; i<number - 1; i++){
if (array[i] < array[i+1])
{
tmp = array[i];
array[i] = array[i+1];
array[i+1] = tmp;
}
}
}
return;
}


// 変数iをグローバルとして渡すが、printfとi++、dummy(10000)の部分をシングルスレッド動作させる
int main(void){
int i, while_count;

i = 0;
while_count = 0;

#pragma omp parallel
while (<<100){

// ブロック内をシングルスレッド動作させる
#pragma omp single
{
printf("Thread %d : %d\n", omp_get_thread_num(), i);
i++;
dummy(10000);
}
printf("Thread %d : loop...\n", omp_get_thread_num());
usleep(10);
while_count++;
}

printf("loop Count: %d\n", while_count);
return;
}



また、「ブロック内を他のスレッドが実行中なら、ブロック内の実行をスキップする」ということも可能。

#include<stdio.h>
#include<unistd.h> // usleep()
#include<stdlib.h>


// ダミー処理関数(CPUに負荷を掛ける)
void dummy(int number){
int n, i, tmp;
int array[number];
for(i=0; i<number; i++){
array[i] = i;
}

for (n=0; n<number; n++){
for (i=0; i<number - 1; i++){
if (array[i] < array[i+1])
{
tmp = array[i];
array[i] = array[i+1];
array[i+1] = tmp;
}
}
}
return;
}


// 変数iをグローバルとして渡すが、printfとi++、dummy(10000)の部分をシングルスレッド動作させる。
// もしその部分が別スレッドで処理中ならスキップする
int main(void){
int i, while_count;

i = 0;
while_count = 0;

#pragma omp parallel
while (i<100){

// single nowait : 他のスレッドがブロック内を実行中だったらスキップ
#pragma omp single nowait
{
printf("Thread %d : %d\n", omp_get_thread_num(), i);
i++;
dummy(10000);
}
printf("Thread %d : loop...\n", omp_get_thread_num());
usleep(10);
while_count++;
}
printf("loop Count: %d\n", while_count);
return;
}


#pragma omp singleと、#pragma omp single nowaitの場合を、
実際にコンパイルして実行、比較してみると、多分違いが分かる…と思う。
また、omp_set_num_threads(スレッド数)で、スレッド数を変更しながらいろいろ試してみるのも良い。


結構長い間やっていた筈なのに、C言語のBlog投稿は今回が初めて。
やっているけど書いていないことって、かなり多いな…。

0 件のコメント: