目次 >> OpenMP
OpenMPは複数のCPU(複数コアを含む)を持った計算機上での並列化に威力を発揮する。
OpenMPを使う最大の利点は、OpenMPに対応したコンパイラであれば、非常に簡単に並列化できる点である。
現在、gcc、Visual C++、およびIntelコンパイラなど主要なコンパイラはOpenMPに対応している。
習得も他の並列化技法に比べて比較的容易である。
なお、速度を最優先にする場合、単一コンピュータ上で動かした場合でも、メモリのローカリティのためかOpenMPよりMPIの方が効率のよいことが多い。MPIに関してはこちらを参照。
なお、インテルがOpenMP初心者向けに非常にわかりやすい文書を公開している。
OpenMPの各種関数を使わない場合、#pragma ompで始まる指示をソースコード内に書き込み、下記のコンパイルスイッチをつけてコンパイルするだけで、並列化される。
インテルコンパイラ(icpc、icl)の場合は、Linux用では-openmpを、Windows用では、/Qopenmpをつけてコンパイルする。なお、インテルコンパイラは、バージョン9よりAMDのプロセッサにも対応するようになったが、バージョン9とAMDプロセッサの組み合わせでのOpenMPはなぜが実行効率が非常に悪い。これはバージョン10にすることにより解決される。
Visual C++(cl)の場合は、/openmpをつけてコンパイルする。
Visual C++は2005よりOpenMPがサポートされている。なお、かつては無料のExpressバージョンには含まれていなかったが、Windows SDK for Windows Server 2008 and .NET Framework 3.5を導入することによって、使えるようになる。詳細はこちら。
最近のVisual C++ Express 2012や2013ではデフォルトで利用可能となっている。
なおMSYSのmakeでコンパイルする場合、-openmpとする。/openmpではエラーが出るので注意。
gcc(g++)の場合は、-fopenmpをつけてコンパイルする。GCCはバージョン4.2から正式にサポートされるようになった。
macOSのgcc(g++)は、初期状態では-fopenmpをつけてコンパイルしてもエラーになる。まず、brewを使って、OpenMPのランタイムライブラリをインストールする。
brew install libomp
コンパイル時には、-fopenmpだけでなく-Xpreprocessorも必要である。またライブラリとして、-lompを指定する必要がある。
ヘッダファイルの場所は
-I"/usr/local/opt/libomp/include"
もしくは
-I"/opt/homebrew/include"
ライブラリの場所は
-L"/usr/local/opt/libomp/lib"
もしくは
-L"/opt/homebrew/lib"
である。
近年のCPUでは高性能コアと高効率コアが混在している。OpenMPは通常、これらのコアの総数をデフォルトのスレッド数とするが、状況によっては、高効率コアの計算終了まで高性能コアの待ちが発生し、逆に遅くなる場合がある。その場合、スレッド数を高性能コア数に合わせるとよい。
ここではOpenMPを使った並列計算について解説する。
OpenMPを使ってコンパイルしているかをプログラム中で知るには、_OPENMPが定義されているかで判別する。具体的には、
#ifdef _OPENMP //OpenMPを使ったコード #else //OpenMPを使わない場合のコード #endif
OpenMPで並列化する場合、最も使われるのがforループの並列化。forループの並列化には#pragma omp parallel forを使う。
#ifdef _OPENMP #pragma omp parallel for #endif for(int i=0;i<N;i++) { //ここの処理を書く }
下記のように書くと毎回forループに入るたびにスレッドの生成コストが発生する。
#ifdef _OPENMP #pragma omp parallel for #endif for(int i=0;i<N;i++) { //ここの処理を書く } #ifdef _OPENMP #pragma omp parallel for #endif for(int i=0;i<N;i++) { //ここの処理を書く } #ifdef _OPENMP #pragma omp parallel for #endif for(int i=0;i<N;i++) { //ここの処理を書く }
下記のようにすると、スレッド生成は一回ですむ。
#ifdef _OPENMP #pragma omp parallel #endif { #ifdef _OPENMP #pragma omp for #endif for(int i=0;i<N;i++) { //ここの処理を書く } #ifdef _OPENMP #pragma omp for #endif for(int i=0;i<N;i++) { //ここの処理を書く } #ifdef _OPENMP #pragma omp for #endif for(int i=0;i<N;i++) { //ここの処理を書く } }
いくつかの異なった処理を並列して行いたい場合は、#pragma omp parallel および#pragma omp sectionsを使う。
#pragma omp parallel #pragma omp sections { #pragma omp section { //並列させたい処理1 } #pragma omp section { //並列させたい処理2 } #pragma omp section { //並列させたい処理3 } }
omp_get_num_procs()を使う。実際にはプロセッサの数*コア数が返される。Hyper-threadingが有効になっているCPUでは、それも含めてカウント(すなわち2倍)される。
#ifdef _OPENMP cout<<"The number of processors is "<<omp_get_num_procs()<<endl; #endif
現在の並列数を取得するには、omp_get_max_threads()関数を使う。
#ifdef _OPENMP cout<<"OpenMP : Enabled (Max # of threads = "<<omp_get_max_threads()<<")"<<endl; #endif
通常は、環境変数OMP_NUM_THREADSから取得して並列数が決まる。omp_set_num_threads関数を使うと、プログラム中で指定できる。
#ifdef _OPENMP omp_set_num_threads(4); #endif
タスクの内容によっては、スケジューリング法を変えることによって、より効率的に多くのCPUを使うことができる。
スケジューリングの指定方法は、
#pragma omp parallel for schedule(type[,size])
の様に、#pragma omp parallel forに続けて指定する。
type指定できるスケジューリングには
の4つがある。このうち、runtimeは実行時に環境変数で指定するという意味で、実際のスケジュール方法としては3つである。
staticはforループが開始される時点で何番目のループがどのスレッドで実行するかが、あらかじめ決まっている。sizeで指定したチャンク数で、順次実行されていく。デフォルトのチャンク数はループ数/プロセッサ数である。もし各スレッドの計算量が均等であれば、staticがもっとも効率がよい。
dynamicはsizeで指定したチャンク数ごとに、あいているスレッドに割り当てられていく。デフォルトのチャンク数は1。ゆえに、dynamicは各スレッドの計算量がばらばらな時に最も有効である。
guidedははじめ大きなチャンク数で割り当て、徐々にチャンク数が小さくなっていくものである。初期にチャンク数が大きいため効率がよい上、最後の方ではチャンク数を小さくして、各プロセッサに極力均等に割り当てるようにするもので、staticとdynamicの中間といったところである。
最終更新日