HPC/並列プログラミングポータルでは、HPC(High Performance Computing)プログラミングや並列プログラミングに関する情報を集積・発信しています。

新着トピックス

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

icon.png

 並列プログラミング向けのコンパイラやデバッガ、各種ライブラリを提供するインテル Parallel Compsoserには、並列プログラミング向けの言語拡張「インテル Cilk Plus」が含まれている。これを利用することで既存のプログラムを容易に並列化したり、より簡潔にアルゴリズムを記述できる。本記事では、このCilk Plusについて機能や使い方を説明する。

Parallel Composerの強力な新機能「インテル Cilk Plus」

 インテル Parallel Studio 2011にはさまざまな新機能が搭載されているが、そのなかでも注目したいのがインテル Parallel Composerに含まれるインテル Cilk Plusだ。Cilk Plusは、次のような特徴を持つ言語拡張だ。

  • ループや連続した処理をキーワード指定だけで容易に並列化できる(cilk_spawn/cilk_sync/cilk_forキーワード)
  • 配列に対する処理の記法が拡張され、「[:]」という表記で複数の配列要素に渡る処理を容易に記述できる
  • 配列を引数に取る関数を容易に並列化できる(elemental function)
  • 最小限のオーバーヘッドで排他制御を行える(reducer)
  • 実装時に生成するスレッド数を考慮する必要がなく、容易にスケーラブルなプログラムを記述できる
  • 覚えるべきキーワードや機能が少ないため習得が容易

 たとえばリスト1はクイックソートをシンプルに実装したものだが、これをCilk Plusで並列化する場合、後半の「my_qsort」関数を再帰呼び出ししている部分に「cilk_spawn」キーワードを付け、最後に「cilk_sync」キーワードを追加するだけで並列化が完了する(リスト2)。

リスト1 クイックソートの例
void my_qsort(int* begin, int* end) {
	int* left = begin;
	int* right = end;
	int n;

	while (1) {
		while (*left  *begin)
			left++;
		while (*begin  *right)
			right--;

		if (left = right)
			break;

		n = *left;
		*left = *right;
		*right = n;
		left++;
		right--;
	}

	if (begin  left-1)
		my_qsort(begin, left-1);
	if (right+1  end)
		my_qsort(right+1, end);
}
リスト2 リスト1をCilk Plusを用いて並列化する
	if (begin  left-1)
		cilk_spawn my_qsort(begin, left-1);  ←「cilk_spawn」を追加
	if (right+1  end)
		my_qsort(right+1, end);
	cilk_sync;  ←「cilk_sync;」を追加

 ごく簡単な修正であるが、筆者の環境(Core 2 Duo E6550/2.33GHz・2コア、メモリ2GB)で1億個の要素を持つint型配列をソートするのに必要な時間を計測・比較したところ、これだけで3割ほどの実行時間短縮が確認できた(表1)。

表1 Cilk Plusによる並列化の結果(ソート実行時間比較)
並列化前並列化後
10611ミリ秒7662ミリ秒

 また、配列に対する演算を次のように簡潔に記述することも可能となる。

	double A[6];
	double B[] = { 1.0, 4.2, 4.2, 5.3, 7.4, 3.2 };
	double C[] = { 3.1, 8.9, 1.4, 2.2, 1.0, 2.4 };
	double d;

	A[:] = 0.0;  ←配列Aの要素をすべて0.0に初期化
	d = __sec_reduce_add(A[:] * B[:]);  ←BとCの内積を計算
	A[:] = B[:] + C[:];  ←A[0] = B[0] + C[0]、 A[1] = B[1] + C[1]、……に相当

 Cilk Plusはこのように非常に簡単に導入でき、また並列化によるパフォーマンスの向上だけでなくコードをより簡潔に記述できるというメリットがある。以下ではこのCilk Plusについて、基本的な機能や文法などのポイントを紹介していく。

Cilk Plusの歴史

 Cilk Plusは元々はMITの「The Cilk Project」により、ANSI Cベースの並列プログラミング言語「Cilk」として開発されていた。Cilkは当初High Performance Computing(HPC)と呼ばれる、スーパーコンピュータなどの高性能コンピュータ向けに開発されていたが、Leiserson教授らが起こしたベンチャー企業Cilk Artsが商用化、C++対応やさまざまな新機能を追加した「Cilk++」としてリリースされた。その後Cilk ArtsはIntelに買収され、仕様変更とともに名称もCilk Plusに変更、Parallel Studioの一部となった。

 なお、CilkはGCCをコンパイラとして利用しており、現在でもGPLでリリースされている。CilkとCilk Plusではspawn/syncキーワードなど共通している部分が多いため、Cilkを利用したことのあるユーザーであればCilk Plusも容易に扱えるだろう。ただし、C++対応やarray extensionsなどClik PlusにはCilkには含まれていない独自の拡張機能も多くあるので、ドキュメントなどで詳細を確認していただきたい。

Cilk Plusを利用する設定

 Cilk Plusは、Parallel Composerに含まれるインテル コンパイラーでのみ利用できる。Cilk Plusを利用したプログラムをVisual Studioでコンパイルする場合、プロジェクトを選択した状態でParallel Compserツールバーの「Use Intel C++」ボタンをクリックし、インテル コンパイラーでプロジェクトをビルドするように設定しておく必要がある(図1)。

 「Use Intel C++」ボタンをクリックすると確認ダイアログが表示されるので「OK」をクリックし、続いてプロジェクトのリビルドを行っておこう(図2

 なお、Cilk Plusを使用したプログラムはVisual StudioのC/C++コンパイラではコンパイルできない。もしソースコードをインテル コンパイラー以外のコンパイラでもコンパイルできるようにしたい場合、次のように「#ifdef」プリプロセッサなどを使い、インテル コンパイラーでのコンパイル時に自動設定される「__cilk」マクロの有無でコードを切り替えるようにしておけば良い。

#ifdef __cilk  /* Cilk Plusが利用できる場合 */
 :
(Cilk Plusを利用するコードを記述)
 :
#else  /* Cilk Plusが利用できない場合 */
 :
(Cilk Plusを利用しないコードを記述)
 :
#endif

Cilk Plusに関するドキュメント

 Cilk Plusに関する情報は、Parallel Stduioヘルプ内の次の個所にまとめられている(図3)。

Intel(R) Parallel Studio 2011
→ Intel(R) Parallel Composer 2011
→Intel(R) C++ Compiler 12.0 User and Reference Guides
→Creating Parallel Applications
→Using Intel(R) Cilk(TM) Plus

 ここにはCilkの機能やリファレンスなど、Cilkを利用するに当たって必要な情報が一通り揃っている。また、インテルWebサイトのCilk Plusページにもサンプルコードや簡単なドキュメントが用意されている。ここにはCilk++からCilk Plusへのマイグレーションに関するドキュメントも用意されているので、Cilk++に関する知識があるユーザーは確認しておくと良いだろう。

 Cilk Plusはリリースされてから日が浅いこともあってか、Web上で公開されている資料などは多くないが、ベースとなっているCilkに関する情報の多くはCilk Plusでも有用だ。たとえばMITのCilk Projectページ内「Documentation」にある、Cilk ProjectのリーダーCharles E. Leiserson氏らによるレクチャー資料は、Cilk Plusの考え方や実装方法を把握するのに有用だろう。

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_spawncilk_spawn指定した処理を並列実行する
_Cilk_synccilk_sync_Cilk_spawnで生成した処理がすべて完了するまで待機する
_Cilk_forcilk_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のようなグラフで表したとき、節と節をつなぐ枝のことを「ストランド(strand)」と呼ぶ。すべてのストランドは並列に実行されることが期待され、また枝と枝が合流する節(cilk_sync;に相当)から開始されるストランドは、その節に合流するすべてのストランドの実行が終了するまで開始されない。

複数スレッド間での変数の安全な共有を提供する「reducer」

 並列プログラミングにおいて、注意しなければならないのが変数の共有方法や排他制御だ。複数スレッドで共有する変数に対しては適切な排他制御が必要であり、それを行わないとバグやパフォーマンス低下を引き起こす原因となる。Clik Plusでは変数に対して汎用的な排他制御の仕組みを提供する「reducer」という機能が備えられている。一般的な変数の代わりにreducerを用いて宣言した変数/オブジェクトを利用することで、並列実行時も整合性が保証される。

 reducerの特徴は次のとおりだ。

  • アクセス時に競合が発生しない、安全な共有変数を提供する
  • ロックを使用しないため、ロックの競合が発生しない
  • 適切に使う限り、並列版と非並列版で同様の挙動を示す
  • 最小のオーバーヘッドで効率的に動作する

 reducerはmutexやセマフォのようなロック機構は使用しない。そのためデッドロックが発生せず、またパフォーマンスも高い。ただし、reducer変数/オブジェクトに対して行える処理は限られており、使用するreducerの種類によって保証される操作は限られる。たとえば後述する「CILK_C_REDUCER_OPADD()」や「cilk::reducer_opadd」で作成したreducerに対しては加算や減算のみが実行可能となる。

 C++では排他制御機構が実装されたテンプレートクラスという形でreducerが実装されており、このクラスのオブジェクトを一般的な変数の代わりとして利用する。たとえばC++で複数スレッドから変数に対し安全に加減算を行いたい場合、「cilk::reducer_opadd」というテンプレートクラスを使用する(リスト6)。テンプレートクラスでは安全に利用できる処理がオペレータ関数として定義されており、その範囲で一般的な変数と同様にアクセスできる。また、reducerオブジェクトの値を取得するには「get_value()」メソッドを用いる。

リスト6 C++でのreducer使用例
#include cilk/cilk.h
#include cilk/reducer_opadd.h  ← reducer_opaddが定義されたヘッダーファイルをinclude

cilk::reducer_opaddint sum;  ← int型の加算reducerを定義

void addsum() {
     sum += 1;
}

int main() {
  sum += 1;
  cilk_spawn addsum();  ← addsum()関数を並列実行
  sum += 1;
  cilk_sync;  ← addsum()関数の終了を待機
  return sum.get_value();  ← 「3」を返す
}

 Cでreducerを利用する場合、reducerの内部処理を実装した関数をラッピングしたマクロ経由でreducer変数を宣言する。たとえば安全に加減算を行うreducerを利用する場合、「CILK_C_REDUCER_OPADD()」マクロを使ってreducer変数を宣言し、変数へのアクセスには「REDUCER_VIEW()」マクロを使用する(リスト7)。

リスト7 Cでのreducer使用例
#include cilk/cilk.h
#include cilk/reducer_opadd.h  ← reducer_opaddが定義されたヘッダーファイルをinclude

CILK_C_REDUCER_OPADD(sum, int, 0);  ← int型の加算reducerを定義

void addsum() {
  REDUCER_VIEW(sum) += 1;
}

int main() {
  REDUCER_VIEW(sum) += 1;
  cilk_spawn addsum();  ← addsum()関数を並列実行
  REDUCER_VIEW(sum) += 1;
  cilk_sync;  ← addsum()関数の終了を待機
  return REDUCER_VIEW(sum);  ← 「3」を返す
}

reducerの動作原理

 reducerではスレッドごとに値を保持する変数を用意することで、複数スレッドからの安全なアクセスを実現している。書き込みアクセスを行う場合はスレッドごとの変数に値を保持し、読み出しアクセスを行う際にはその時点で各スレッドの変数で保持されている値を集計し値を返す、という処理を行っている。これにより並列で実行した場合でも非並列で実行した場合と同じ値を返すことが保証され、また同一の変数に書き込みアクセスが同時に発生することを避けることができる。

 ただし、浮動小数点演算の処理に関しては、処理によっては丸め誤差などのため結果に違いが出ることがある。厳密な計算を要するものなど、プログラムによっては許容できない誤差が出ることがあるため注意してほしい。また、reducerを使用することによるパフォーマンス低下は一般的には少ないものの、たとえば大量のreducerオブジェクトを使用する場合、最悪の場合同時に実行されているスレッド分だけの変数が割り当てられるため、大きなオーバーヘッドが生じる可能性がある。

 そのほか、読み出しアクセスを行う際には各スレッドごとの変数から集計を行う必要があるため、同時に多くのスレッドが動作している状況で頻繁に読み出しアクセスを行うとパフォーマンス低下の恐れがある。

あらかじめ用意されているreducer

 Cilk Plusではあらかじめ表3のようなreducerが用意されている。また、既存のマクロやテンプレートをベースに独自のreducerを実装することも可能だ。

表3 あらかじめ用意されているreducer
テンプレートクラス(C++)マクロ(C)オブジェクトの型オブジェクトに対し行える操作
reducer_list_append-リストリスト末尾への追加(push_back())
reducer_list_prepend-リストリスト先頭への追加(push_front())
reducer_maxCILK_C_REDUCER_MAX配列配列の最大値の取得(cilk::max_of())
reducer_max_indexCILK_C_REDUCER_MAX_INDEX配列配列の最大値のインデックス取得(cilk::max_of())
reducer_minCILK_C_REDUCER_MIN配列配列の最小値の取得(cilk::min_of())
reducer_min_indexCILK_C_REDUCER_MIN_INDEX配列配列の最小値のインデックス取得(cilk::min_of())
reducer_opaddCILK_C_REDUCER_OPADD数値加算(+=、=、-=、++、--)
reducer_opandCILK_C_REDUCER_OPAND数値AND演算(、=、=)
reducer_oporCILK_C_REDUCER_OPOR数値OR演算(|、|=、=)
reducer_opxorCILK_C_REDUCER_OPXOR数値XOR演算^、^=、=)
reducer_ostream-出力ストリーム出力ストリームへの書き込み()
reducer_basic_string、reducer_string、reducer_wstring-文字列(string)文字列末尾への追加(+=、append())

Cilk Plusを使用したコードの非並列実行(Serialization)

 Cilk Plusを使用したコードは、コンパイル時にコンパイルオプションとして「/Qcilk-serialize」、もしくは「/FI cilk/cilk_stub.h」を指定することで、cilk_spawnやcilk_forによる並列処理を無効化し、非並列のプログラムとして実行できる。プログラムが正しく動作せず、並列化による問題が疑われる場合、このオプションを使用してコンパイルを行って実行結果を確認してみると良いだろう。

配列に対する処理をより簡潔に表す配列表記

 Cilk Plusではcilk_spawnやcilk_sync、cilk_forといった並列処理を指定するキーワードだけでなく、配列操作に関する表記についても拡張されている。これは「C/C++ Extensions for Array Notation(CEAN)」などと呼ばれており、「[]」を使ったC/C++の配列表記に新たな表現を追加するものだ。これを利用することで、配列の初期化や代入、配列要素同士の計算などを簡潔に記述できる。

 Cilk Plusでは、次のようにして配列の複数要素に対し一括処理を行える。

<配列変数>[<開始要素>:<長さ>]
<配列変数>[<開始要素>:<長さ>:<間隔>]

 たとえば、配列aに対し3番目から6番目までの4つの要素に10という値を代入する場合、次のように表記できる。

a[3:4] = 10;  ← a[3]、a[4]、a[5]、a[6]に10を代入

 上記の例では連続した要素にアクセスしているが、間隔を指定することで非連続した要素に対しても一括アクセスできる。

b[3:4:2] = 10;  ← b[3]、b[5]、b[7]、b[9]に10を代入

 開始要素と長さの両方を省略した場合、配列のすべての要素へのアクセスとなる。

c[:] = 10;  ← cのすべての要素に10を代入

 それぞれの要素に同じ値を代入するだけでなく、別の配列の値を代入することも可能だ。ただし、要素の数が異なる場合はエラーとなる。

a[3:2] = b[2:2];  ← a[3]にb[2]を、a[4]にb[3]を代入
c[:] = d[:];  ← 配列cに配列dの内容をコピー
e[3:3] = f[3:2]  ← 要素の数が異なるのでエラーとなる

 代入だけでなく各種演算も行える。また、変数で対象となるインデックスを指定することも可能だ。

a[3:2] = b[2:2] * 10;  ← a[3]にb[2]*10を、a[4]にb[3]*10を代入
a[0:10] = b[0:10] *c[5:10];  ← a[0]にb[0]*c[5]、a[1]にb[1]*c[6]、...を代入
a[:] = b[:] + 10;  ← 配列bの各要素に10を加えたものを配列aに代入
a[i:2] = b[i:2];  ← a[i]にb[i]を、a[i+1]にb[i+1]を代入

 これらの表記は、多次元配列に対しても適用できる。

a[:][:] = b[:][:];
c[2:4][2:3] = d[4:4][2][0:3] * 10;

 操作対象とする要素を配列で指定することも可能だ。このような操作は、「Gather」や「Scatter」などと呼ばれる。

unsigned index[] = { 1, 0, 3, 2 };
a[0:4] = b[index[0:4]];  ← a[0] = b[1]、 a[1] = b[0]、a[2] = b[3]、a[3] = b[2]という代入操作に相当
c[index[0:4]] = d[0:4];  ← c[1] = d[0]、 c[0] = d[1]、c[3] = d[2]、c[2] = d[3]という代入操作に相当

 また、条件分岐を含む文についても記述できる。たとえば、次のリスト8のようなコードは、リスト9のように記述することができる。

リスト8 if文を含む配列操作
for (int i = 0; i  n; i++) {
    if (a[i]  b[i]) {
        c[i] = a[i] - b[i];
    } else{
        d[i] = b[i] - a[i];
    }
}
リスト9 リスト7を「[:]」を使って記述する例
if (a[:]  b[:]) {
   c[:] = a[:] - b[:];
} else {
   d[:] = b[:] - a[:];
}

// 下記のようにも記述できる
c[0:n] = (a[0:n]  b[0:n]) ? a[0:n] - b[0:n] : c[0:n];
d[0:n] = (a[0:n] = b[0:n]) ? b[0:n] - a[0:n] : d[0:n];

 なお、これらの表記を利用する場合、事前に配列のサイズが明示的に指定されている必要がある。関数内などで静的に宣言されている配列については問題ないが、malloc()などで動的に確保した配列に対しては、明示的な型変換が必要となる。そのような場合、C99規格で規定されている可変長配列を使用すると良い(リスト10)。C99を使用するには「/Qstd=c99」オプションを付けてコンパイルを行う必要がある。

リスト10 動的に確保した配列に対する操作(C99の可変長配列を使用)
int* foobar(int length) {
	typedef int (*array_type)[len];
	int* ptr = (int*) malloc(sizeof(int) * length);  ← 長さlengthの配列を割当
	array_type a = (array_type) ptr;  ← ポインタを配列に変換
	(*a)[:] = 10;  ← 各要素に10を代入
	return ptr;
}

 「:」を使った表記はCPUのSIMD命令を用いて高速に処理され、またSIMD命令で実行できないような処理に関しても可能な限り並列で実行されるため、簡潔にコードを記述できるだけでなくパフォーマンス面でもメリットがある。

配列の全要素を順番に処理するリダクション関数

 Cilk Plusでは「リダクション関数」という、配列を引数に取って各要素の和や積、最小値/最大値などを求める関数が用意されている。たとえば「__sec_reduce_add()」は、引数として与えた配列の全要素の和を返す関数だ。

int a[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
int sum;

sum = __sec_reduce_add(a[:]);  ← aの全要素を加算する
printf("%d\n", sum);  ←「55」が表示される

 Cilk Plusでは加算だけでなく、乗算や最大値/最小値の演算、ゼロ/非ゼロを判断するなど、さまざまなリダクション関数が用意されている(表4)。

表4 Cilk Plusで提供されているリダクション関数
関数プロトタイプ説明
__sec_reduce_add(a[:])すべての要素の和を返す
__sec_reduce_mul(a[:])すべての要素の積を返す
__sec_reduce_all_zero(a[:])すべての要素がゼロの場合真を、そうでない場合偽を返す
__sec_reduce_all_nonzero(a[:])すべての要素が非ゼロの場合真を、そうでない場合偽を返す
__sec_reduce_any_nonzero(a[:])要素のどれか1つ以上が非ゼロの場合真を、そうでない場合偽を返す
__sec_reduce_min(a[:])要素中の最小値を返す
__sec_reduce_max(a[:])要素中の最大値を返す
__sec_reduce_min_ind(a[:])要素中で最小の値を持つ要素のインデックスを返す
__sec_reduce_max_ind(a[:])要素中で最大の値を持つ要素のインデックスを返す
__sec_reduce(fun, identity, a[:])各要素を引数として順番に関数funを実行し、その結果を返す

 用意されているリダクション操作だけでなく、「__sec_recuce()」関数を利用することで任意のリダクション処理を記述することも可能だ。__sec_reduce()関数では指定した配列の各要素を引数として、繰り返し指定した関数を呼び出し、最後に呼び出された関数の戻り値を__sec_reduce()の戻り値として返す、という動作を行う(図5)。

 たとえばint型の配列に対し、それぞれの要素の2乗の和を返すようなリダクション操作を行う場合、与える関数funは次のようになる。

/* 第1引数(new_value)には次の要素の値が、
第2引数(previous_total)には直前に実行されたfun()の戻り値が与えられる /*
int fun(int new_value, int previous_total) {
  // 与えられた要素の値を2乗し、直前のfun()の戻り値に加算して返す
  return new_value*new_value + previous_total;
}

 これらリダクション関数はあらかじめ並列化が行われているため、たとえばforループなどを使って独自に同様の処理を記述するよりも高速に実行される。配列に対し連続してアクセスするような場合、これらの関数が利用できないか検討してみると良いだろう。

配列の全要素に同じ処理を実行するScalar Function Maps

 配列のすべての要素に対し、同一の処理を実行したいという場合に有用なのが「Scalar Function Maps」と呼ばれる機能だ。たとえば、配列aの各要素を引数にsin()を実行し、その結果を配列bに格納したいという場合、次のように記述できる。

a[:] = sin(b[:]);

 あらかじめsin()やcos()、pow()といった数学関数が定義されているほか、新たに関数を定義して使用することもできる。

a[:] = pow(b[:], c);  ← a[:] = b[:]**cに相当
a[:] = pow(c, b[:]);  ← a[:] = c*:に相当

// ユーザー定義関数
__declspec(vector) float foo(float a, float x, float y) {
    return (a * x + y);
}
a[:] = foo(10.0, x[:], y[:]);

 ユーザー定義の関数を使用する場合、呼び出される関数(Elemental Functionと呼ばれる)は「__declspec(vector)キーワードを付加して宣言する。また、Elemental Functionでは下記が使えないという制約があるので注意してほしい。

  • forやwhile、do、gotoといったループ
  • switch文
  • クラスや構造体に対する処理
  • 非elemental functionsの呼び出し
  • _Cilk_spawn
  • [:]を使った配列操作

 Scalar Function Mapsでは自動的に並列化が行われるほか、数学関数についてはSIMDライブラリが呼ばれるため、より高速に実行できるのが特徴だ。

手軽で使いやすく、高パフォーマンスが期待できるCilk Plus

 Cilk Plusは簡潔で直感的に分かりやすい仕様を備えており、OpenMPなどほかの並列プログラミング技術と比べると覚えるべきキーワードなども少なく、比較的習得が容易であると思われる。また、CとC++の両方で利用できるのもメリットだ。

 また、Parallel Studio 2011では全面的にCilk Plusサポートが取り込まれている。たとえばパフォーマンス解析ツールであるParallel InspectorにはCilk Plusを使用したプログラムの解析に対応しているほか、並列化支援ツールであるParallel AdvisorではCilk Plus向けのコーディングガイドが用意されている。Parallel Studioを利用しており、これから新たにプログラムを並列化する、という場合、Cilk Plusは並列プログラムを実装するツールとして第1の候補に挙げられるだろう。