インテル Parallel Composerの新機能――並列プログラムを容易に実装できる「インテル Cilk Plus」入門 3ページ

Cilk Plusのキーワード

 Cilk Plusによる並列プログラミングでもっとも基本となるのが、「_Cilk_spawn」や「_Cilk_for」、「_Cilk_sync」キーワードを使った並列化だ。これらのキーワードで指定した処理やループの並列実行、そして処理の完了待ちを行える(表2)。なお、ソースコード中で「cilk/cilk.h」ではこれらはそれぞれ「cilk_spawn」や「cilk_sync」、「cilk_for」としてdefineされており、こちらを利用することが推奨されている。ドキュメントなどでは両方が混在している場合があるので注意して欲しい。

表2 Clik Plusで用意されている並列プログラミングのためのキーワード
キーワード 別名 説明
_Cilk_spawn cilk_spawn 指定した処理を並列実行する
_Cilk_sync cilk_sync _Cilk_spawnで生成した処理がすべて完了するまで待機する
_Cilk_for cilk_for 並列実行させたいループを指定する(forループの代わりに使用する)

並列処理を開始するcilk_spawnと終了待ちを行うcilk_sync

 並列実行したい処理に対し次のようにcilk_spawnキーワードを付けることで、その処理を並列実行できる。

cilk_spawn func1(a, b, c);    ← 関数「func1(a, b, c)」を並列実行する
d = cilk_spawn func2(a, b, c);    ← 関数「func2(a, b, c)」を並列実行し、結果を変数dに格納する

 cilk_spawnキーワードで処理の並列実行を開始した後、その処理の終了を待機するにはcilk_syncキーワードを使用する。

cilk_sync;

 たとえば、ある関数「funcA()」と「funcB()」を並列実行させたい場合、コードは次のリスト3のようになる。

リスト3 cilk_spawnとcilk_syncの使い方

 :
 :
cilk_spawn funcA();    ← funcAを並列実行させる
funcB();    ← funcAの終了を待たず、funcBが実行される
cilk_sync;    ← funcAとfuncBの終了を待つ

 このとき、次のように関数の戻り値を直接他の関数の引数として与えることはできない。

    g(cilk_spawn f());    ← このような記述はエラーとなる

 このような場合、C++の拡張機能であるラムダ関数を使って次のように記述すれば良い。

cilk_spawn [&]{ g(f()); }();

 ラムダ関数は現在策定中のC++新規格「C++0x」で標準化が予定されているC++の言語拡張であるが、Parallel Composerに含まれるインテル コンパイラーではすでに実装されており利用が可能だ。C++0xについては「インテル コンパイラーで試す次世代C++規格「C++0x」という記事で紹介しているので、興味のある方はこちらを参照していただきたい。

 なお、これらの表記を次のように書いた場合エラーにはならないものの、「f()を実行した後、その戻り値を引数にg()を並列実行する」という動作となってしまう。

cilk_spawn g(f());

ループを並列実行するcilk_for

 並列プログラミングでよく使われるテクニックとして、forループ内の処理を並列実行する、というものがある。cilk_forキーワードは、このような処理を簡潔に記述するためのキーワードだ。

 たとえば、次のリスト4のようなコードを並列実行する例を考えよう。

リスト4 並列化されていないforループの例

int i, end;
 :
 :
for (i = 0; i < end; i++) {
    func(i);
}

 この場合、リスト5のようにコード中の「for」を「cilk_for」に書き換えるだけで、forループ内が並列実行されるようになる。

リスト5 cilk_forで並列化されたループ

int i, end;
 :
 :
cilk_for (i = 0; i < end; i++) {    ← ループ内が並列実行される
    func(i);
}

 ただし、cilk_forの利用にはいくつかの制限があり、すべてのforループを無条件に置き換えられるわけではない。

  • 初期化式(1つ目の引数、リスト5の例では「i = 0」の部分)では単一の変数に対する初期化処理しか記述できない(ここで初期化した変数は「制御変数」と呼ばれる)
  • 条件式(2つ目の引数、リスト5の例では「i < end」の部分)では制御変数に対する比較を行わなければならない。また、使用できる比較演算子は「<」および「<=」、「!=」、「>=」、「<」のみで、さらに比較対象とする値はループ中変更されてはいけない
  • 加算式(3つ目の引数、リスト5の例では「i++」の部分)では制御変数に対する加算/減算を行わなければならない。許可されている操作は「+=」および「-=」、「++」、「–」のみで、また「+=」および「-=」を使用する場合、加減算する値はループ中変更されてはいけない

 なお、cilk_forの引数にSTLのイテレータを使用することも可能だ。たとえば、次のようなforループはそのままcilk_forループに書き換えることができる。

for (T::iterator i(vec.begin()); i != vec.end(); ++i) {
    func(i);
}

 一方、次のようなforループはcilk_forに置き換えられない。

// cilk_forに置き換えられない例1:複数の変数を初期化している
for (i = 0, j = 0; i < end; i++) {
      func(i);
}
// cilk_forに置き換えられない例2:条件式内で関数を呼んでいる
for (i = 0; compare_func(i); i++) {
      func(i);
}
// cilk_forに置き換えられない例3:制御変数で加算される値が一定でない
for (i = 0; i < end; i += calc(i)) {
      func(i);
}

 そのほか、cilk_forループ内ではcilk_for/cilk_syncの利用や__try、__except、__finally、__leaveによる例外処理も禁止されており、これらを利用しているforループもcilk_forに置き換えられない。

Cilk Plusでの処理の流れ

 Cilk Plusを用いた並列処理の流れは、有向無閉路グラフ(Directed Acyclic Graph、DAG)というグラフで図示できる。たとえば次のリスト6のようなプログラムの場合、処理の流れは図4のように図示できる。

リスト6 Cilk Plusを用いたプログラム例

 :
 :
funcA();
cilk_spawn funcB();    ←�
funcC();
cilk_spawn funcD();    ←�
funcE();
cilk_sync;    ←�
funcF();
 :
 :
図4 リスト6の実行の流れ
図4 リスト6の実行の流れ

 プログラムの流れを図4のようなグラフで表したとき、節と節をつなぐ枝のことを「ストランド(strand)」と呼ぶ。すべてのストランドは並列に実行されることが期待され、また枝と枝が合流する節(cilk_sync;に相当)から開始されるストランドは、その節に合流するすべてのストランドの実行が終了するまで開始されない。