共有メモリマシン上での並列計算プログラミング
金田憲二
背景 速く計算ができると色々うれしいはず 一つ一つのプロセッサはまだまだ遅い
並列に計算することで高速化 1 個のプロセッサで 1000 時間かかる計算
を、 1000 個のプロセッサを使って 1 時間で !!!
どうやって並列計算をするのか 並列計算機を手に入れなければいけな
い プログラムを記述しなければいけない※ 当然、実現したい計算の種類によって
最適な手段は異なる
並列計算機の種類 共有メモリマシン
例) SunFire15K
クラスタ ネットワークでつながった数百~数千台の計算
機例) IBM BladeCenter例) Sun V210 x 128
並列計算を記述する方法 普通の言語
例) C/Java + スレッド / 通信ライブラリ とても大変
自動並列化コンパイラ ユーザは並列計算を意識しないで済む 性能などの面で問題がある
並列プログラミング言語例) Cilk 、 Linda 、 KLIC 、 Stackthreads 、… 簡便に高性能な並列計算を実現可能
今回取りあげるのは Cilk 言語
90 年代後半に MIT で研究・開発 C 言語にいくつかのキーワードを追加 共有メモリマシン上で動作 再帰関数などは結構簡単に並列化
例)チェスの次手の探索
発表の概要~ Cilk 言語のチュートリアル~ フィボナッチの並列化 言語の詳細 実装の概要
フィボナッチの並列化
フィボナッチ数を計算するC プログラム
#include <stdlib.h>
#include <stdio.h>
int fib (int n) {
if (n<2) return (n)
int x, y;
x = fib (n-1);
y = fib (n-2);
return (x+y);
}
int main (int args, char *argv[]) {
int n, result;
n = atoi(argv[1]);
result = fib (n);
printf(“Result: %d\n”, result);
return 0;
}
並列に計算できる箇所は?
fib (6);
fib (5);
fib (4);
fib (3);
fib (2);
fib (4);
fib (3);
fib (3);
fib (2);
fib (1);
fib (0);
fib (2);
fib (1);
fib (2);
fib (1);
並列に計算できる箇所は?#include <stdlib.h>
#include <stdio.h>
int fib (int n) {
if (n<2) return (n)
int x, y;
x = fib (n-1);
y = fib (n-2);
return (x+y);
}
int main (int args, char *argv[]) {
int n, result;
n = atoi(argv[1]);
result = fib (n);
printf(“Result: %d\n”, result);
return 0;
}
この 2 つの関数の呼び出しは
並列に実行できる
Cilk で記述された並列フィボナッチ
#include <stdlib.h>
#include <stdio.h>
#include <cilk.h>
cilk int fib (int n) {
if (n<2) return (n)
else {
int x, y;
x = spawn fib (n-1);
y = spawn fib (n-2);
sync;
return (x+y); } }
cilk int main (int args, char *argv[]) {
int n, result;
n = atoi(argv[1]);
result = spawn fib (n);
sync;
printf(“Result: %d\n”, result);
return 0;
}
プログラム中に現れるキーワード cilk spawn sync
cilk spawn や sync を呼び出す関数の先
頭につける識別子
cilk int f() {
…
}
※ 以降この識別子のついた関数を cilk procedure と呼ぶ
spawn
並列関数呼び出し 関数の終了を待たずに呼び出し元が実行を
続ける
spawn f (x);
g ();
f (x) の終了を待たずに g () も実行する
sync
spawn した関数の終了を待つ
y = spawn f (x);
…
sync;
printf (“%d”, y);
f (x) の終了を待つ
コンパイル コマンド名: cilk
gcc と同じコンパイルオプションが使える$ cilk -g -Wall -O2 fib.cilk -o cilk
$ cilk filename
実行 --nproc で計算機のプロセッサ数を指定
例) 64 プロセッサ上での fib (32) の計算$ ./fib --nproc 64 32
※fib (32) の計算は SunFire 15K 上で 約 55 倍のスピードアップ
$ filename --nproc n args
プロファイリング (1/2)
以下のオプションをつけてコンパイル --cilk-profile --cilk-critical-path
以下のオプションをつけて実行 --stats level
プロファイリング (2/2)$ cilk -cilk-profile -cilk-critical-path -O2 fib.cilk -o fib
$ ./fib --nproc 4 --stats 1 30
Result: 832040
RUNTIME SYSTEM STATISTICS
Wall-clock running time on 4 processors: 2.593270 s
Total work = 10.341069 s
Total work (accumulated) = 7.746886 s
Critical path = 779.588000 us
Parallelism = 9937.154003
言語の詳細
言語の詳細 Storage allocation Locking Inlets Aborting Timer (プログラム例: n-queens )
Storage Allocation (1/2)
Stack memory への割り当て Cilk procedure 内では
C 関数内では
※ 関数が return する時に自動的に開放される
ptr = Cilk_alloca(size) ;
ptr = alloca(size) ;
Storage Allocation (2/2)
Heap memory への割り当て 通常の C と同じ
ptr = malloc(size) ;
free(ptr) ;
Locking
Cilk は相互排他ロックを提供する スレッド間で共有されるデータへの
atomic なアクセスを可能にする
Data race の例 (1/2)
cilk int foo (void) {
int x = 0;
spawn bar(&x);
x = x + 1;
sync;
return (x);
}
cilk void bar (int *px)
{
*px = *px + 1;
return;
}
foo() が 2 を返す(ことを意図した)プログラム
Data race の例 (2/2) 実行によっては、値が正しく更新されない
cilk int foo (void) {
int x = 0;
spawn bar(&x);
x = x + 1;
sync;
return (x);
}
cilk void bar (int *px)
{
*px = *px + 1;
return;
}
(1) x を read (= 0)
(4) x へ write (= 1)
(2) x を read (= 0)
(3) x へ write (= 1)
(5) 1 をreturn
API (1/2)
型
ロックを初期化する関数
ロックを取得する関数
ロックを開放する関数
Cilk_lockvar
Cilk_lock_init( l )
Cilk_lock ( l )
Cilk_unlock( l )
API (2/2)
ロックを取得できるのは同時に一つの スレッド
ロックを取得できなかったスレッドは そのロックが開放されるまで待機
プログラム例 1#include <cilk-lib.h>
Cilk_lockvar mylock;
{
Cilk_lock_init(mylock);
Cilk_lock(mylock);
/* critical section
(atomic に実行したいコード ) */
Cilk_unlock(mylock);
}
プログラム例 2#include <cilk-lib.h>
Cilk_lockvar mylock;
cilk int foo (void) {
int x = 0;
Cilk_lock_init(mylock);
spawn bar(&x);
Cilk_lock(mylock);
x = x + 1;
Cilk_unlock(mylock);
sync;
return (x); }
cilk void bar (int *px)
{
Cilk_lock (mylock);
*px = *px + 1;
Cilk_unlock(mylock);
return;
}
Inlets
spawn の返り値に使える操作というものが制限されている
そのために spawn の返り値を操作するための特殊な機構
spawn に対する制限 (1/2)
spawn の返り値に対して許されている操作は
= *= /= %= += -=
&= ^= |= <<= >>= のみ y += spawn f (x);
spawn に対する制限 (2/2)
以下のような操作は許されていないz = spawn f (x) + spawn g (y)
ただし h は C の関数h (spawn f (x));
Inlet spawn の返り値を処理する特殊な関数
Cilk procedure 内で定義される spawn の返り値を渡すことができる
cilk int f (int x) {
inlet int g (int y) {
…
}
z = g (spawn h())
}
プログラム例cilk int fib (int n) {
int x = 0;
inlet void summer (int result) {
x += result;
}
if (n < 2) return n;
summer (spawn fib (n-1));
summer (spawn fib (n-2));
sync;
return (x);
}
注意点 (1/2)
inlet 中で spawn や sync を呼び出すことはできない
cilk int f (int x) {
inlet int g (int y) {
z = spawn h();
}
z = g (spawn h())
}
注意点 (2/2)
Implicit atomicity spawn の呼び出したスレッドと同じスレ
ッドが inlet 内を処理するcilk int fib (int n) {
inlet void summer (int result) {
x += result;
}
summer (spawn fib (n-1));
summer (spawn fib (n-2));
}
x へのアクセスはatomic であることが保障されている
Aborting
spawn した Cilk procedure を中断させる機構 探索の枝狩りなどの目的に使用する
Abort inlet 内に abort プリミティンブをいれる
cilk int f (int n) {
inlet void g (int x) {
...
abort;
}
…
}
プログラム例cilk void search(int n) {
inlet void catch() {
if ( 解が発見された ) { abort; }
…
}
for (x in 探索空間 ) {
catch (spawn search (x));
}
}
注意点 abort が呼ばれた時点でまだ spawn
されていないタスクは中断されない
cilk void foo(int n) {
inlet void bar() {
abort;
}
bar (spawn baz(17));
spawn baz(28);
}
(3) abort を呼び出す
(1) baz(17) をspawn
(4) baz(28) をspawn
(2) baz(17) が終了
Timer
型
wall-clock time を返す関数
Cilk_time を秒に変換する関数
Cilk_time
t = Cilk_get_wall_time()
sec = Cilk_time_sec(t)
プログラム例: n-queens (1/8)
N×N のチェスボード上で以下の条件を満たす N 個の Queen の配置を求める[ 条件 ]Queen 同士は互いを取れない
Q
Q
Q
Q
Q
Q
Q
Q
プログラム例: n-queens (2/8)
#include <stdlib.h>#include <stdio.h>#include <string.h>#include <cilk.h>#include <cilk-lib.h>
…
プログラム例: n-queens (3/8)
…
int safe(char *config, int i, int j) { int r, s; for (r = 0; r < i; r++) { s = config[r]; if (j==s || i-r==j-s || i-r==s-j) { return 0; } } return 1;}
…
プログラム例: n-queens (4/8)
…
cilk char *nqueens(char *config, int n, int i) { char *new_config; char *done = NULL; int j;
inlet void catch(char *res) { if (res != NULL) { if (done == NULL) { done = res; } abort; } }
…
プログラム例: n-queens (5/8)
…
if (i==n) { char *result;
/* put this good solution in heap, and return a pointer to it */ result = malloc(n*sizeof(char)); memcpy(result, config, n*sizeof(char)); return result; }
…
プログラム例: n-queens (6/8)
…
/* try each possible position for queen <i> */ for (j=0; j<n; j++) { new_config = Cilk_alloca((i + 1) * sizeof(char)); memcpy(new_config, config, i*sizeof(char)); if (safe(new_config, i, j)) { new_config[i] = j; catch(spawn nqueens(new_config, n, i+1)); } if (done != NULL) { break; } } sync;
return done; }…
プログラム例: n-queens (7/8)
…
cilk int main(int argc, char *argv[]) { int n; char *config; char *result;
n = atoi(argv[1]); config = Cilk_alloca(n*sizeof(char));
result = spawn nqueens(config, n, 0); sync;
…
プログラム例: n-queens (8/8)
…
if (result != NULL) { int i; printf("Solution: "); for (i=0; i<n; i++) { printf(“%2d\n”, result[i]); } } else { printf(“No solutions!\n”); } return 0;}
実装の概要
実装の概要 work-stealing による負荷分散
idle なプロセッサはランダムに選んだプロセッサからタスクを盗む
実装の概要 work-stealing による負荷分散
idle なプロセッサはランダムに選んだプロセッサからタスクを盗む
実装の概要 work-stealing による負荷分散
idle なプロセッサはランダムに選んだプロセッサからタスクを盗む
参考文献 Cilk 5.3.2 Reference Manual
Supercomputing Technologies Group MIT Laboratory for Computer Science
http://supertech.lcs.mit.edu/cilk 2001
コンパイル方法
foo.cilkcilk2c
Cilk runtime system
gcc ldfoo.c foo.o foo
Top Related