gccのOpenMP実現について | たけおか ぼちぼち日記

たけおか ぼちぼち日記

思いついたらメモ


GCCのOpenMP実現について

OpenMPは、C言語をちょこっと拡張した言語だ。
Cのソースコードに、#pragma を追加するだけで、もとのプログラムを、手軽に並列化できる。

SMPなマルチコアのCPUが一般的な現在、普通の gcc が OpenMP対応になっている。

gccのOpenMPでは、実行時に環境変数の設定が大事だ。詳しくは、後半の 2011/DEC/27 加筆部分を参照。


なお、ここで使用した例題については、全面的に、
http://www.hpcs.cs.tsukuba.ac.jp/~taisuke/EXPERIMENT/openmp-txt.pdf
から頂いています。

OpenMP自体についても、上の文書などを参照してください。
あと、gccのOpenMPについての詳しくは↓の文書を参照。
http://gcc.gnu.org/onlinedocs/gcc-4.4.5/libgomp.pdf


ここで使うソフトウェアなど
OS: 普通の Ubuntu 10.10
gcc: Ubuntu 10.10 に付属の普通の gcc version 4.4.5 (Ubuntu/Linaro 4.4.4-14ubuntu5)
CPUは x86の2コア。





1. hello worldを、コア数の分だけ、並列実行する例。

「#pragma omp parallel」で指定したブロックが、コア数だけスレッド生成され、並列に実行される。


ソース hello.c


% cat hello.c

#include
main()
{
#pragma omp parallel
{
printf("hello world from %d of %d\n",
omp_get_thread_num(), omp_get_num_threads());
}
}


コンパイル

% gcc -fopenmp hello.c

実行

% ./a.out
hello world from 0 of 2
hello world from 1 of 2


2コアのx86で実行したので、2つのスレッドが生成され、printf()が2つ実行された。


さて、gccが生成した
hello.s を見よう。


Cソース上で、#pragmaで指定したブロックは、gccによって関数として括り出され、main.omp_fn.0 として 23行から存在する。



12 movl $main.omp_fn.0, (%esp) 並列化するブロックが関数になった。その関数を引数とする。
13 call GOMP_parallel_start OpenMPのスレッドを生成
14 movl $0, (%esp)
15 call main.omp_fn.0 #pragmaで指定したブロックが、関数として独立させられている
16 call GOMP_parallel_end OpenMPが生成した全スレッドがjoinして一つに


13行、GOMP_parallel_start という OpenMPのランタイム・ルーチンがスレッドを生成する。
GOMP_parallel_start には、全スレッドが実行すべき関数 main.omp_fn.0 を引数として渡す。
GOMP_parallel_startの引数は、この他に、スレッドごと独立なデータ領域のポインタ(この例では NULL)、スレッド数(IFが存在したり、その結果により、数を0,1などにする。この例では0)がある。

16行、GOMP_parallel_end でOpenMPが生成した全スレッドをjoinさせて、一つのスレッドに戻る。




2. 単純なベクタ(一次元配列)の加算を、コア数の分だけ並列実行する例。

「#pragma omp parallel」で指定したブロックが、コア数だけスレッド生成され、並列に実行される。

「#pragma omp for reduction(+:s)」は、全計算の結果が、sという変数に加算しながら集約(縮退, reduction)されることを、指定する。
現在のgccの実現では、連続するiを各スレッドに割り当てる。
詳しくは、sum-s.s の説明で。


ソース sum.c

% cat sum.c

int A[1000];
main()
{
int i;
for(i = 0; i < 1000; i++) A[i] = i;
printf("sum = %d\n", sum(A,1000));
}

int sum(int *a, int n)
{
int s, i;
s = 0;
#pragma omp parallel
{
#pragma omp for reduction(+:s)
for(i = 0; i < n; i++) s += a[i];
}
return s;
}


コンパイル

% gcc -fopenmp sum.c
sum.c: In function ‘main’:
sum.c:6: warning: incompatible implicit declaration of built-in function ‘printf’


実行

% ./a.out
sum = 499500


2コアのx86で実行したので、2つのスレッドが生成されているが、それは見えない。

さて、gccが生成した
sum.s を見よう。


今回の例では、2コアに、各スレッドが割当たる。
現在のgccの実現では、連続する i を各スレッドに割り当てる。
今回のプログラムでは、
0~999をCPUのコア数で割る。今回は2コアなので、2で割る。
0~499を1つのスレッド(コア)に、500~999を2つめのスレッド(コア)に割り当てる。
そして、各スレッドごとに、ローカルな s に A[i]を加算していく。
各スレッドは、計算が終わると全体の和(ここでは、「(%edx)」)に、アトミック(排他的)に加算を行う。
最後に、GOMP_barrierで、全スレッドの終了を待ち合わせる。

なぜアトミックな加算が必要か。アトミックな加算とは何か?

ここで、アトミックでない場合の問題を、記述する。
0) ここで、s = s + a を行うとする。a はスレッド・ローカルで、s はグローバル。
説明のために、s_new = s_old + a とする。(実際には、s_newとs_oldは同一の変数である)
1)あるスレッドt1がs_old を読み出し、加算を行っているが、まだs_newへのストアを行っていない時。
2)その時に、別なスレッドt2が同じ地点に到達して、s_oldの読み出しを行った場合、s_oldには、スレッドt1の結果が反映されていない。
3)スレッドの実行速度がほぼ同じであれば、t1がs_newにストアし、その後t2がs_newにストアする。しかし、t2がストアしたs_newからは、t1の結果が失われている。
(スレッドのストア実行の時刻が逆でも、どちらかのスレッドの結果が失われることに変わりはない)

s=s+aの加算がアトミックでないから、上記のような問題が起きる。
s_oldを読み出し始めてから、s_newへのストアまでを、分割が不能(不可分=アトミック)に行えば、問題は解決する。

「lock addl」命令は、s=s+aをアトミック(不可分)に行う命令である。
この命令により、複数のスレッドが同時に、s=s+a を行おうとしても、正しく実行される。




OpenMPは、分散環境の機械で不用意に使用した場合には、速くならないどころか、より遅くなることも多々あると思われる。
しかし、4コアや8コアのマルチコア SMPマシンが手軽に入手できる現在、
既存のC, Fortranのプログラムを、手軽に並列化して、マルチコアの恩恵にあずかれるのは、いいことであろう。

なにせ、普通の gcc に勝手に入ってしまっているのだから。






OSとgccなどの詳しいバージョンなど


% uname -a
Linux gyopo 2.6.35-27-generic #48-Ubuntu SMP Tue Feb 22 20:25:29 UTC 2011 i686 GNU/Linux


% gcc -v
Using built-in specs.
Target: i686-linux-gnu
Configured with: ../src/configure -v --with-pkgversion='Ubuntu/Linaro 4.4.4-14ubuntu5' --with-bugurl=file:///usr/share/doc/gcc-4.4/README.Bugs --enable-languages=c,c++,fortran,objc,obj-c++ --prefix=/usr --program-suffix=-4.4 --enable-shared --enable-multiarch --enable-linker-build-id --with-system-zlib --libexecdir=/usr/lib --without-included-gettext --enable-threads=posix --with-gxx-include-dir=/usr/include/c++/4.4 --libdir=/usr/lib --enable-nls --with-sysroot=/ --enable-clocale=gnu --enable-libstdcxx-debug --enable-objc-gc --enable-targets=all --disable-werror --with-arch-32=i686 --with-tune=generic --enable-checking=release --build=i686-linux-gnu --host=i686-linux-gnu --target=i686-linux-gnu
Thread model: posix
gcc version 4.4.5 (Ubuntu/Linaro 4.4.4-14ubuntu5)


CPU

cpu family : 6
model : 23
model name : Intel(R) Core(TM)2 Duo CPU P8400 @ 2.26GHz
stepping : 6
cpu MHz : 2260.728
cache size : 3072 KB
cpu cores : 2







2011/DEC/27 以下を超絶に大加筆。

機械もOSも新しくした。新たな気持ちで openMPした。


% uname -a
Linux gyopo 3.0.0-14-generic #23-Ubuntu SMP Mon Nov 21 20:28:43 UTC 2011 x86_64 x86_64 x86_64 GNU/Linux


take@gyopo% gcc -v
Using built-in specs.
COLLECT_GCC=gcc
COLLECT_LTO_WRAPPER=/usr/lib/gcc/x86_64-linux-gnu/4.6.1/lto-wrapper
Target: x86_64-linux-gnu
Configured with: ../src/configure -v --with-pkgversion='Ubuntu/Linaro 4.6.1-9ubuntu3' --with-bugurl=file:///usr/share/doc/gcc-4.6/README.Bugs --enable-languages=c,c++,fortran,objc,obj-c++,go --prefix=/usr --program-suffix=-4.6 --enable-shared --enable-linker-build-id --with-system-zlib --libexecdir=/usr/lib --without-included-gettext --enable-threads=posix --with-gxx-include-dir=/usr/include/c++/4.6 --libdir=/usr/lib --enable-nls --with-sysroot=/ --enable-clocale=gnu --enable-libstdcxx-debug --enable-libstdcxx-time=yes --enable-plugin --enable-objc-gc --disable-werror --with-arch-32=i686 --with-tune=generic --enable-checking=release --build=x86_64-linux-gnu --host=x86_64-linux-gnu --target=x86_64-linux-gnu
Thread model: posix
gcc version 4.6.1 (Ubuntu/Linaro 4.6.1-9ubuntu3)



CPU

processor : 0
vendor_id : GenuineIntel
cpu family : 6
model : 42
model name : Intel(R) Core(TM) i5-2520M CPU @ 2.50GHz
stepping : 7
cpu MHz : 800.000
cache size : 3072 KB



gccのopenMPでは、実行時に環境変数の設定が大事だ。

今回の実測に使ったプログラムのソースは、このsum1.c。(非常に単純過ぎるものである)
% gcc -O4 -fopenmp -o sum-omp sum1.c
としてコンパイル。
(OpenMP無しは % gcc -O4 -DNO_OMP -o sum-cc sum1.c )


OMP_NUM_THREADS=2
OMP_SCHEDULE=static
GOMP_CPU_AFFINITY=0 3

これの条件で、OpenMPを2スレッド並列で実行。
スレッドのCPUへの割り当ては明示的に、0番CPUと3番CPUを指定。
Linuxでは、物理コア0の2つのhyper threadに、CPU番号の0番,1番が割り当てられる。
同様に、物理コア1の2つのhyper threadに、CPU番号の2番,3番が割り当てられる。
CPUの0番と3番を指定して、別々の物理コアにスレッドが割当たるようにしている。
(2番でなく、3番を指定したのは、気の迷い。ちなみに、0番と2番を指定しても、私の環境では、同様だった)

(OMP_SCHEDULE=staticの効き目については、今後、勉強する)


これで、実測すると。
elapsed time: 1.27sec
user time : 2.07sec
CPU usage : 198.0%

(OpenMP無しの実測値は、最後にあり)

----

次は、試しに、同じ物理コアに2スレッドを割り当ててみる。
(CPU指定を0番,1番とし、物理コア0)

OMP_NUM_THREADS=2
OMP_SCHEDULE=static
GOMP_CPU_AFFINITY=0 1

実測結果。
elapsed time: 1.90sec
user time : 3.26sec
CPU usage : 196.2%

物理コアを分けて割り当てたものより、確かに遅い。
CPUの稼働率も物理コアを分けたものより低い。
Hyper threadはそもそもアクティビティは1で、同じALUを使うので、そんなに速くない。
今回、196%も使えているのは、今回のテストが大きなデータを順番に舐めるので、キャッシュの効きが悪く、メモリIOが多く、裏スレッドがうまく動いているのであろう。
それでも、OpenMP無しよりは、速い。


---
4スレッド(2物理コア、4Hyper thread)で、実行すると。

OMP_NUM_THREADS=4
OMP_SCHEDULE=static


elapsed time: 1.05sec
user time : 3.52sec
CPU usage : 390.1%

おお、なんだかんだ言っても、もっとも速いではないかっ!

今回は、テスト・プログラムがいい加減で、hyper threadだけでも速くなったから、それが物理コアが2つで、それ相応に速くなったのだろう。

CPU使用率が390%とは、すごく得した気分だなぁ。


---
OpenMP無しは、次のとおり。

elapsed time: 2.34sec
user time : 1.99sec
CPU usage : 99.0%

--

今回の例で、発生したページフォールトは、
OpenMP実行(どれも):
1465065minor pagefaults 程度(+/- 1程度のぶれが出る)

OpenMP無し:
1464989minor pagefaults 程度(-1のぶれが出る)

ページフォールトは、システムが実時間を消費する。
OpenMPだからといって、特に異常にフォールトが増加することは無いようだ。
OpenMP版は、スレッド生成/ジョインなどのランタイム・ルーチンを走行するので、フォールトが増加するのは元来的に仕方ない。