共有メモリマシン上での 並列計算プログラミング 金田憲二 背景 速く計算ができると色々うれしいはず 一つ一つのプロセッサはまだまだ遅い 並列に計算することで高速化 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> int main (int args, char *argv[]) { #include <stdio.h> int n, result; int fib (int n) { n = atoi(argv[1]); if (n<2) return (n) result = fib (n); int x, y; printf(“Result: %d\n”, result); x = fib (n-1); return 0; y = fib (n-2); return (x+y); } } 並列に計算できる箇所は? fib (4); fib (5); fib (3); fib (6); fib (3); fib (4); fib (2); fib (3); fib (2); fib (2); fib (1); fib (2); fib (1); fib (1); fib (0); 並列に計算できる箇所は? #include <stdlib.h> int main (int args, char *argv[]) { #include <stdio.h> int n, result; int fib (int n) { n = atoi(argv[1]); if (n<2) return (n) result = fib (n); int x, y; printf(“Result: %d\n”, result); x = fib (n-1); return 0; y = fib (n-2); return (x+y); } } この2つの関数の呼び出しは 並列に実行できる Cilkで記述された 並列フィボナッチ #include <stdlib.h> cilk int main (int args, char *argv[]) { #include <stdio.h> int n, result; #include <cilk.h> n = atoi(argv[1]); cilk int fib (int n) { result = spawn fib (n); if (n<2) return (n) sync; else { printf(“Result: %d\n”, result); int x, y; x = spawn fib (n-1); y = spawn fib (n-2); sync; return (x+y); } } 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; f (x)の終了を待つ printf (“%d”, y); コンパイル コマンド名:cilk $ cilk filename gccと同じコンパイルオプションが使える $ cilk -g -Wall -O2 fib.cilk -o cilk 実行 --nprocで計算機のプロセッサ数を指定 $ filename --nproc n args 例)64プロセッサ上でのfib (32)の計算 $ ./fib --nproc 64 32 ※fib (32)の計算はSunFire 15K上で 約55倍のスピードアップ プロファイリング (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内では ptr = Cilk_alloca(size); C関数内では ptr = alloca(size); ※関数がreturnする時に自動的に開放される Storage Allocation (2/2) Heap memoryへの割り当て 通常のCと同じ ptr = malloc(size); free(ptr); Locking Cilkは相互排他ロックを提供する スレッド間で共有されるデータへのatomicな アクセスを可能にする Data raceの例 (1/2) foo()が2を返す(ことを意図した)プログラム cilk int foo (void) { int x = 0; { spawn bar(&x); *px = *px + 1; x = x + 1; return; sync; return (x); } cilk void bar (int *px) } Data raceの例 (2/2) 実行によっては、値が正しく更新されない cilk int foo (void) { int x = 0; (1) xをread (= 0) spawn bar(&x); cilk void bar (int *px) (2) xをread (= 0) { *px = *px + 1; x = x + 1; return; (3) xへwrite (= 1) sync; (4) xへwrite (= 1) return (x); } } (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 void bar (int *px) cilk int foo (void) { { int x = 0; Cilk_lock (mylock); Cilk_lock_init(mylock); *px = *px + 1; spawn bar(&x); Cilk_unlock(mylock); Cilk_lock(mylock); return; x = x + 1; Cilk_unlock(mylock); sync; return (x); } } Inlets spawnの返り値に使える操作というもの が制限されている そのためにspawnの返り値を操作するた めの特殊な機構 spawnに対する制限 (1/2) spawnの返り値に対して許されている操作は = *= /= %= += -= &= ^= |= <<= >>= のみ y += spawn f (x); spawnに対する制限 (2/2) 以下のような操作は許されていない z = spawn f (x) + spawn g (y) h (spawn f (x)); ただしhはCの関数 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; } xへのアクセスはatomicで あることが保障されている summer (spawn fib (n-1)); summer (spawn fib (n-2)); } 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) { (3) abortを呼び出す inlet void bar() { abort; } bar (spawn baz(17)); spawn baz(28); } (1) baz(17)をspawn (2) baz(17)が終了 (4) baz(28)をspawn Timer 型 Cilk_time wall-clock timeを返す関数 t = Cilk_get_wall_time() Cilk_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.cilk cilk2c foo.c gcc foo.o ld foo Cilk runtime system
© Copyright 2025 ExpyDoc