高速化の解説一覧:[link:高速化]
処理を分担し並列計算させることで,時間のかかる処理も高速化できる場合があります.処理を並列化させたい場合,当然ながらコード上に並列化のための記述を加える必要があるのですが,OpenMPではシンプルな記述のみで並列化を実現できます.
*OpenMP とは
並列化を行うための拡張言語です.CPUによる並列化を実行できます.{{small:GPUによる並列化は含みません.}}
対応言語:C/C++,Fortran
対応コンパイラ:gcc, Clang,Microsoft Visual C++,Intel Compiler(C++/Fortran)
OpenMPの利用方法については,下記の資料が参考になります.
{{small:[1]Rest Term::Tech Note (要点が抑えられていて非常に参考になります)}}
{{small:[link:https://rest-term.com/technote/index.php/OpenMP#content_1_9] }}
{{small:[2]北山, "高速化プログラミング入門", カットシステム, 2016 (OpenMPについては,基本的な内容のみです)}}
{{small:[link:https://www.amazon.co.jp/高速化プログラミング入門-北山-洋幸/dp/4877833870] }}
{{small:[3]openmp.org 公式の仕様書}}
{{small:[link:https://www.openmp.org/specifications/] }}
*OpenMP の導入
まずは,OpenMPを利用するコードのコンパイル方法について解説します.
動作検証用として,次のコードを使いたいと思います.
{#
#include <stdio.h>
#include <omp.h>
int main() {
printf("使用可能な最大スレッド数:%d\n", omp_get_max_threads());
#pragma omp parallel for
for (int i = 0; i < 10; i++) {
// (何かの処理)
// 検証用の表示
printf("thread = %d, i = %2d\n", omp_get_thread_num(), i);
}
return 0;
}
#}
細かい解説は後でしますが,#pragma omp ~ と書いてあるのが,OpenMP特有の並列化の指示文です.
動作検証用のプログラムが準備できたところで,次にコンパイル方法を説明します.
開発環境によって異りますが,基本はコンパイルオプションを設定するだけなので簡単です.
**gcc
-fopenmp とコンパイルオプションを設定すればコンパイルできます.
例:
C++
{/
$ g++ main.cpp -fopenmp
/}
C言語
{/
$ gcc main.c -fopenmp
/}
**Clang
gccと同じで,-fopenmp とコンパイルオプションを設定すればコンパイルできます.
**visual studio
プロジェクトのプロパティを設定するだけです.
具体的には,
[プロパティ] -> [構成プロパティ] -> [C/C++] -> [言語] -> [OpenMPサポート]
から[はい (/openmp)]を選択します.
**Intel Compiler
-openmp とコンパイルオプションを設定すればコンパイルできます.
**Cmake
Cmake経由でプロパティを有効化させた場合,CmakeListに次のような記述を加えれば良いです.
{#
find_package(OpenMP)
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${OpenMP_C_FLAGS}")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${OpenMP_CXX_FLAGS}")
#}
*OpenMP の基礎
ここでは,OpenMPの基本的な利用方法を解説します.
**for指示文
forループを並列化する方法について解説します.
まずは,導入編で使ったコードを見てみます.
{#
#include <stdio.h>
#include <omp.h>
int main() {
printf("使用可能な最大スレッド数:%d\n", omp_get_max_threads());
#pragma omp parallel for
for (int i = 0; i < 10; i++) {
// (何かの処理)
// 検証用の表示
printf("thread = %d, i = %2d\n", omp_get_thread_num(), i);
}
return 0;
}
#}
ここで,#pragma omp parallel for とあるのが,forループを並列実行させるための指示文です.
また,omp_get_thread_num()は各ループを処理しているスレッドの番号を返します.
{{small:omp_get_thread_num() や omp_get_max_threads() などのランタイム関数を使うには,omp.hのインクルードが必要です.#pragma omp ~ の並列化の指示文のみを使う場合は,omp.hは不要です}}
プログラムの実行結果は,例えば以下のようになります.{{small:(実行するPCによってスレッドの最大数や表示の順番は変わります)}}
{/
使用可能な最大スレッド数:4
thread = 0, i = 0
thread = 2, i = 6
thread = 0, i = 1
thread = 1, i = 3
thread = 0, i = 2
thread = 2, i = 7
thread = 3, i = 8
thread = 1, i = 4
thread = 3, i = 9
thread = 1, i = 5
/}
上記の表示だと異なる番号のスレッドが入り乱れていて分かりにくいため,下記のように整理してみます.
{/
thread = 0, i = 0
thread = 0, i = 1
thread = 0, i = 2
thread = 1, i = 3
thread = 1, i = 4
thread = 1, i = 5
thread = 2, i = 6
thread = 2, i = 7
thread = 3, i = 8
thread = 3, i = 9
/}
各ループ(i = 0~9)を4つあるスレッドで分担して処理されていることが分かると思います.
***並列化を実行するスレッド数
#pragma omp parallel for と記述すると,利用可能なすべてのスレッドを利用して並列化を行います.
これに対し,以下のように num_threads(N) の記述を追加すると,並列化を実行するスレッド数を制御できます.{{small:Nには任意の定数を設定する}}
{# #pragma omp parallel for num_threads(2)#}
出力の例
{/
使用可能な最大スレッド数:4
thread = 0, i = 0
thread = 0, i = 1
thread = 0, i = 2
thread = 1, i = 5
thread = 0, i = 3
thread = 1, i = 6
thread = 0, i = 4
thread = 1, i = 7
thread = 1, i = 8
thread = 1, i = 9
/}
今回の例では,余った2つのスレッドは何もしません.実際には,意図的にスレッドを余らせて,さらに別の処理を実行させたい場合などにスレッド数を設定することになると思います.
***2重ループ
画像処理を行うプログラムなどでは2重ループを良く使うと思います.
原則的には,下記のように最も外側のループで処理を並列化させる方法が効果的です.
{#
#pragma omp parallel for
for (int v = 0; v < 480; v++) {
for (int u = 0; u < 640; u++) {
// (何かの処理)
}
}
#}
一応,下記のように内側に設定することも可能です.ただしこの場合,ループ内で何度も並列化を実行することになるので処理のオーバーヘッド※がかさみます.
{#
for (int v = 0; v < 480; v++) {
#pragma omp parallel for
for (int u = 0; u < 640; u++) {
// (何かの処理)
}
}
#}
{{small:※オーバーヘッドとは,ある処理を行う時に間接的にかかってしまうコストのことです.今回の例では,並列化に伴う前準備などの処理になります.通常その処理は重くはないのですが,何度も実行するとそれなりに負担になります.これを抑えるため,最も外側のループで並列化を実行するというのが一般的です.}}
***atomic
下記のように,ある変数に異なるスレッドからアクセスする例を考えます.
変数に値を代入する際にタイミング良く処理が競合すると,想定外の結果につながる場合があります.
{#
int data = 0;
#pragma omp parallel for
for (int i = 0; i < 10; i++) {
// (何かの処理)
data = data +(何かの値)
}
#}
書き込み時に処理の競合が発生しないようにするためには,atomic 指示文を利用します.
#pragma omp atomic と記述した下の1文は複数のスレッド間で同時に実行されないように指定できます.
{#
int data = 0;
#pragma omp parallel for
for (int i = 0; i < 10; i++) {
// (何かの処理)
#pragma omp atomic
data = data +(何かの値)
}
#}
**section指示文
別々に実行可能ないくつかの処理を並列化する方法について解説します.
{#
#include <stdio.h>
#include <omp.h>
int main() {
printf("使用可能な最大スレッド数:%d\n", omp_get_max_threads());
#pragma omp parallel sections
{
#pragma omp section
{
// (何かの処理)
printf("thread = %d task0\n", omp_get_thread_num());
}
#pragma omp section
{
// (何かの処理)
printf("thread = %d task1\n", omp_get_thread_num());
}
}
return 0;
}
#}
実行例
{/
使用可能な最大スレッド数:4
thread = 0 task0
thread = 1 task1
/}
使い方としては,#pragma omp parallel sections と記述したあと,分割する処理の前に #pragma omp section と記述することで,各処理を並列化できます.
**OpenMP のマクロ
OpenMPのコンパイルオプションを設定すると マクロ _OPENMP が有効になります.
OpenMPを有効化するときにだけ追加で処理を行いたい場合は,例えば次の様に記述すると良いと思います.
{#
#ifdef _OPENMP
// (何かの処理)
#endif
#}
各指示文のOn/Offを切り替えたい場合は,例えば次の様に新しくマクロを定義するのが良いと思います.
{#
#define _USE_OMP_PARALLEL_FOR
// ~~~
#ifdef _USE_OMP_PARALLEL_FOR
#pragma omp parallel for
#endif
for (int i = 0; i < 10; i++) {
// (何かの処理)
}
#}
>> ご意見・ご質問など お気軽にご連絡ください.info