openmp でマルチコア 並列 プログラミングtau/lecture/computer_software/... ·...
TRANSCRIPT
OpenMP でマルチコア (並列)プログラミング
田浦健次朗
1 / 28
並列プログラミングの必要性
▶ いまどきの計算機にはことごとく, 複数の CPU (コア) が搭載れている
▶ 普通にプログラムを書いただけでは, その内の一つのCPU しか使われない
▶ 複数の CPU を使うプログラム (並列プログラム) を書いてみよう
2 / 28
CPU の数の確かめ方 (Linux)▶ 方法 1: /proc/cpuinfo を見る�1 $ cat /proc/cpuinfo
▶ 方法 2 (GUI): パネル上の system load indicator → 右クリック → 「システムモニタを開く」
▶ 方法 3: hwloc-ls コマンド (おそらくインストールが必要)�1 $ hwloc-ls
インストール:�1 $ sudo apt-get install hwloc
▶ 方法 4: htop コマンド (おそらくインストールが必要)�1 $ htop
3 / 28
CPU を「数える」時の (ややこしい) 用語▶ 仮想コア, ハードウェアスレッド ⊂ (物理) コア ⊂ チップ
▶ チップ: 物理的なチップの数. ラップトップは普通 1 個▶ (物理) コア: 一チップ内に複数 (今は 2-8 程度) 搭載.命令デコーダ, 演算器などを一揃い揃えたもの.
▶ 仮想コア: 一つの物理コア内に複数 (今は 1-2 程度) 搭載. 命令デコーダを独立に持っている.
▶ ソフトウェア (/proc/cpuinfo, hwlos, etc.) から見える“CPU” ≈ 仮想コア
▶ 物理的に「CPU」というとチップを指すことが多い (ややこしい…)
memory
controller L3 cache
hardware thread
(virtual core, CPU)
(physical) core
L2 cacheL1 cache
chip (socket, node, CPU)
4 / 28
並列プログラミング
やり方は色々▶ OS が直接提供するインタフェース
▶ プロセス (プログラムを複数立ち上げる)▶ スレッド (Pthread)
▶ 言語▶ OpenMP (← 今回はこれ)▶ Cilk
▶ ライブラリ▶ Intel Threading Building Block
5 / 28
OpenMP の使い方
1. C/C++ プログラムに並列化の 「並列化指示 (ディレクティブ, API 呼び出し)」を挿入
2. gcc でコンパイルするときに -fopenmp オプションを与える�
1 $ gcc -fopenmp program.c
3. 基本はこれだけ!4. 以降「並列化指示」の構文, 使い方を説明する
6 / 28
先に例を見せておく▶ クイックソートの再帰呼び出しを並列に行う例▶ 変更は赤字部分を追加するだけ�
1 /* a[l:r]を整列 */2 void qs(K * a, long l, long r) {3 if (r - l <= 1) return;4 else {5 /* 「ある値」以下を左へ, より大きいのを右へ */6 long c = partition(a, l, r);7
8 qs(a, l, c-1); /* 左を整列 */9 qs(a, c, r); /* 右を整列 */
10
11 }12 }
7 / 28
先に例を見せておく▶ クイックソートの再帰呼び出しを並列に行う例▶ 変更は赤字部分を追加するだけ�
1 /* a[l:r]を整列 */2 void qs(K * a, long l, long r) {3 if (r - l <= 1) return;4 else {5 /* 「ある値」以下を左へ, より大きいのを右へ */6 long c = partition(a, l, r);7 #pragma omp task if (r - l > 100)8 qs(a, l, c-1); /* 左を整列 */9 qs(a, c, r); /* 右を整列 */
10 #pragma omp taskwait11 }12 }
8 / 28
覚えるべき構文
▶ #pragma omp parallel▶ #pragma omp master▶ #pragma omp task▶ #pragma omp taskwait▶ #pragma omp for
正式な情報源:▶ http://openmp.org/ → OpenMP Specifications▶ OpenMP Specifications ページに日本語訳もあり▶ 書籍: 日本語も含め多数
9 / 28
指示文の構文: 共通事項
▶ C/C++ 構文を拡張するのではなく, C/C++ の文を修飾する指示文 (directive, pragma)
▶ 指示文は #pragma omp …という文法. 例:�1 #pragma omp parallel2 文
▶ 一部, 関数もある
10 / 28
parallel (仕様書 2.4)
▶ これがなくては何も始まらない▶ 構文:�1 #pragma omp parallel2 S
▶ 意味: S (parallel の直下に書かれた文) を, 参加する全てのCPU が実行
S S S S
...
...
11 / 28
parallel : 例 1�1 #include <stdio.h>2 int main() {3 printf("main\n");4 #pragma omp parallel5 printf("worker\n");6 }�1 $ gcc -fopenmp hellopmp.c2 $ ./a.out3 main4 worker5 worker6 worker7 worker
12 / 28
OpenMP の基本的な実行モデル▶ プログラム開始時は一 CPU だけで実行されている (「逐次」領域)
▶ pragma omp parallel に遭遇すると, 「並列領域 (「全 CPU が」実行するモード)」に移行; 全 CPUが「直下の一文」を実行する; 終わったらまた逐次領域に戻る
▶ 直下の一文と言っても複合文, 関数呼び出しを含むなど, 実際にはいくらでも「長い」部分を並列領域にできる
▶ ここで並列領域に参加しているCPU を「チーム」と呼ぶ
S S S S
...
...
13 / 28
parallel : 例 2
�1 #include <stdio.h>2 int main() {3 printf("main\n");4 #pragma omp parallel5 parallel_main();6 }
▶ real_main の中身 (したがってプログラムのほぼ全て) を全 CPU が実行する
14 / 28
OMP_NUM_THREADS
▶ プログラム実行時に環境変数 OMP_NUM_THREADS を指定すると, 並列領域で用いる CPU 数を指定できる
▶�1 $ gcc -fopenmp hellopmp.c2 $ OMP_NUM_THREADS=2 ./a.out3 main4 worker5 worker
15 / 28
master (仕様書 2.8)
▶ 構文�1 #pragma omp master2 S
▶ 意味: S を, (並列領域内であっても) ただひとつの CPU (その並列領域を生んだ CPU) だけが実行する
▶ 何の役に立つ? 後でわかる
...
...
#pragma omp
S
16 / 28
parallel, master : 例�
1 #include <stdio.h>2 int main() {3 printf("main\n");4 #pragma omp parallel5 {6 printf("para 1\n");7 #pragma omp parallel8 printf("master\n");9 printf("para 2\n");
10 }11 }
�1 $ OMP_NUM_THREADS=2 ./a.out2 main3 para 14 master5 para 26 para 17 para 2
17 / 28
parallel だけではろくな事はできない
▶ parallelだけでは「全 CPU に同じ事をやらせる」のがせいぜい
▶ チーム内で「仕事を分け合」わなくては意味がない (高速化はしない)
▶ そのための二つの手段▶ pragma for (ループの分割)▶ pragma task, pragma taskwait (タスクの生成)
▶ OpenMP を使う場合, pragma parallel for を使うことが圧倒的に多い
▶ が, 今の我々に重要なのは後者 (task)
18 / 28
task, taskwait
▶ タスク ≈ 他の CPU に分散させたい処理の単位▶ task 構文 ≈ タスクを作る▶ taskwait 構文 ≈ タスクの終了を待つ
19 / 28
task (仕様書 2.7)▶ 構文:�1 #pragma omp task2 S
▶ 意味: S を実行する「タスク」を作る ≈ チーム内の暇な CPU を見つけて S を実行
▶ 結果的に�1 #pragma omp task2 S3 C
は, S と C を (並行に) 実行する(かも)
...
...
#pragma task
S #pragma task
TS
T
20 / 28
taskwait (仕様書 2.8)
▶ 構文:�1 #pragma omp taskwait
▶ 意味: 現タスクから作られたタスクの完了を待つ
21 / 28
再掲: クイックソートの再帰呼び出しを並列に�
1 /* a[l:r]を整列 */2 void qs(K * a, long l, long r) {3 if (r - l <= 1) return;4 else {5 /* 「ある値」以下を左へ, より大きいのを右へ */6 long c = partition(a, l, r);7 #pragma omp task8 qs(a, l, c-1); /* 左を整列 */9 qs(a, c, r); /* 右を整列 */
10 #pragma omp taskwait11 }12 }
22 / 28
全体の構成
▶ parallel で, チームを作り▶ master で, そのうち一人だけが qs の実行を開始▶ その中でタスクが作られ, チーム内に負荷分散して行く�
1 int main() {2 #pragma omp parallel3 #pragma omp master4 {5 a = ...6 qs(a, 0, n);7 }8 }
23 / 28
実行イメージ
▶ qs の再帰呼び出し内でタスクが作られている
▶ タスクも再帰的に (極めて多数) 作られる
▶ OpenMP がそれを「空いている CPU に割り当てて実行」してくれる
...
...
...
...
24 / 28
実行イメージ
▶ タスクは多数作っても構わないが, あまりにも小さい (少ししか計算をしない) 処理にタスクを作っても利益はない
▶ 少なくとも, 「タスクを作るオーバーヘッド < 処理そのもの」でないと意味がない
▶ task 構文には, 実際にタスクを作るかどうかの条件を書ける�
1 #pragma omp task if (...)
25 / 28
再再掲: クイックソートの再帰呼び出しを並列に�
1 /* a[l:r]を整列 */2 void qs(K * a, long l, long r) {3 if (r - l <= 1) return;4 else {5 /* 「ある値」以下を左へ, より大きいのを右へ */6 long c = partition(a, l, r);7 #pragma omp task if (r - l > 100)8 qs(a, l, c-1); /* 左を整列 */9 qs(a, c, r); /* 右を整列 */
10 #pragma omp taskwait11 }12 }
26 / 28
注: そもそも何でも並列化して良いわけではない▶ 以下により, qs(a, l, c-1) と qs(a, c, r) が「並行に」実行され (得) る�
1 #pragma omp task if (r - l > 100)2 qs(a, l, c-1); /* 左を整列 */3 qs(a, c, r); /* 右を整列 */4 #pragma omp taskwait5 }6 }
▶ これが OK (そうやっても結果が変わらない) なのは, 両者の読み書きする領域が重なっていないため
▶ qs(a, l, c-1); → a[l:c-1]▶ qs(a, c, r); → a[c,r]
27 / 28
並列化して良い (ひとつの十分) 条件�1 A;2 B;
を�1 #pragma omp task2 A;3 B;4 #pragma omp taskwait
と書き換えて良いか?
A
B
AB
A;
B;
#pragma omp task
A;
B;
#pragma omp taskwait
ひとつの十分条件は,1. A が書く領域 ∩ B が読むか書く領域 = ∅ かつ2. B が書く領域 ∩ A が読むか書く領域 = ∅
28 / 28