提供されているレデューサーで要件を満たすことができない場合は、カスタム・レデューサーを記述することができます。
新しいレデューサーを作成する際には、提供されているレデューサーを参考にすることができます。ただし、一部複雑なものもあります。各レデューサーのコードは、インストール・ディレクトリーの include/cilk サブディレクトリーにある reducer_*.h ヘッダーファイルに格納されています。
次のセクションで、その他の例も紹介します。
レデューサーのコンポーネント
レデューサーは、論理的にこれらのコンポーネントに分けることができます。
"View" クラス。レデューサーのプライベート・データです。ランタイムシステムが呼び出せるように、コンストラクターとデストラクターはパブリックでなければなりません。コンストラクターでは、そのレデューサーの View を "単位元" に初期化するようにします。単位元については、後で詳しく説明します。View クラスは、レデューサー・クラスをプライベート・データ・メンバーへのアクセスが可能なフレンドクラスとすることも、またはアクセス手段を提供することもできます。
"Monoid" クラス。モノイドとは、数学的な概念です。モノイドの定義については、後で説明します。Monoid クラスは、cilk::monoid_base<View> から派生し、次の宣言を持つ reduce という名前の static または const のパブリックメンバーを含んでいなければなりません。
static void reduce (View *left, View *right);
オプション: cilk::reducer<Monoid> オブジェクトを含み、安全な (結合的な) 操作でのみ使用可能なラッパークラス。 慣例により、レデューサーの現在のビューを返す get_value() メンバーがあります。
レデューサー・ライブラリーのレデューサーは、View クラスと Monoid クラスに class ではなく struct を使用しています。 C++ では、struct と class は、デフォルトのアクセスが違うだけです。class はプライベートで、struct はパブリックです。
単位元
単位元とは、別の値と組み合わせたときに、組み合わせる順序に関係なく (両側単位元)、組み合わせた値に等しくなる値のことです。次に例を示します。
x = 0 + x = x + 0
x = 1 * x = x * 1
"abc" = "" concat "abc" ="abc" concat ""
Monoid
数学的に、モノイドは値のセット (タイプ)、セットに対する結合操作、およびそれらのセットと操作ための単位元からなります。例えば、(integer, +, 0) や (real, *, 1) などです。
レデューサー・ライブラリーでは、Monoid はT 型として定義され、次の 5 つの関数を提供します。
reduce(T *left, T *right) |
*left = *left OP *right を評価します。 |
identity(T *p) |
初期化されていない *p へ IDENTITY の値を設定します。 |
destroy(T *p) |
p のポイント先のオブジェクトに対してデストラクターを呼び出します。 |
allocate(size) |
生メモリーの size バイトへのポインターを返します。 |
deallocate(p) |
p にある生メモリーを解放します。 |
この 5 つの関数は static または const でなければなりません。 通常、Monoid の要件を満たすクラスには状態がありませんが、単位元オブジェクトの初期化に使用する状態がある場合もあります。
monoid_base クラス・テンプレートは、単位元が型 T のデフォルト値で作成され、new 演算子を使用して割り当てられる、大きなセットの Monoid クラスに役立つ基本クラスです。 monoid_base の派生クラスは、reduce 関数のみ宣言および実装する必要があります。
reduce 関数は、"左側" のインスタンスに "右側" のインスタンスのデータをマージします。 ランタイムシステムが reduce 関数を呼び出した後に、"右側" のインスタンスを破棄します。
決定性のある結果を得るためには、reduce 関数は結合則を満たす操作 (可換でなくてもかまいません) を実装する必要があります。正しく実装された reduce 関数は、シリアル・セマンティクスを保持しています。 つまり、アプリケーションをシリアル実行した結果または 1 つのワーカーで実行した結果と、複数のワーカーで実行した結果が同じになります。ランタイムシステムは、結合則を満たす reduce 関数とともに、ワーカーやプロセッサー・コアの数、またはストランドのスケジュール方法に関係なく、結果が同じになることを保証します。
レデューサーの記述方法 - "Holder" サンプル
この例は、簡単なレデューサーの記述方法を示します。このレデューサーには、update メソッドがなく、reduce メソッドは何もしません。 "Holder" は、"スレッド・ローカル・ストレージ" に似ていますが、「 OS スレッドとの相互作用」で説明されているマイナス面がありません。
完全に同期されるまで get_value() を呼び出さないという規則は、ここでは意図的に破られています。
場合によっては、このようなレデューサーも役に立ちます。この例では、for ループの各反復でグローバルな一時バッファーが使用されています。これは、シリアルプログラムでは安全ですが、for ループが並列の cilk_for ループに変換されると安全ではなくなります。
グローバルな一時変数 temp を使用して値をスワップし、point 要素の配列を逆にする次のプログラムについて考えてみます。
#include <cilk/cilk.h>
#include <cstdio>
class point
{
public:
point() : x_(0), y_(0), valid_(false) {};
void set (int x, int y) {
x_ = x;
y_ = y;
valid_ = true;
}
void reset() { valid_ = false; }
bool is_valid() { return valid_; }
int x() { if (valid_) return x_; else return -1; }
int y() { if (valid_) return y_; else return -1; }
private:
int x_;
int y_;
bool valid_;
};
point temp; // temp is used when swapping two elements
int main(int argc, char **argv)
{
int i;
point ary[100];
for (i = 0; i < 100; ++i)
ary[i].set(i, i);
cilk_for (int j = 0; j < 100 / 2; ++j)
{
// reverse the array by swapping 0 and 99, 1 and 98, etc.
temp.set(ary[j].x(), ary[j].y());
ary[j].set (ary[100-j-1].x(), ary[100-j-1].y());
ary[100-j-1].set (temp.x(), temp.y());
}
// print the results
for (i = 0; i < 100; ++i)
std::printf ("%d: (%d, %d)\n", i, ary[i].x(), ary[i].y());
return 0;
}
このプログラムでは、グローバル変数 temp で競合が発生しますが、シリアルプログラムは問題なく動作します。 cilk_for() の内側で temp を宣言すると簡単で安全ですが、 ここでは、次のような "holder" レデューサーを実装して使用し、ストランドごとにローカルのストレージを提供します。
以下のソリューションは、"holder" レデューサーを実装して使用します。
point_holder クラスというのが "holder" レデューサーです。 point クラスをビューとして使用します。 Monoid クラスには reduce 関数がありますが、ここではどちらのバージョンが保持されてもかまわないので、この関数は何もしません。 point_holder の残りのメソッドを使用して、レデューサー・データにアクセスしたり、更新したりします。
#include <cilk/reducer.h>
class point
{
// Define the point class here, exactly as above
};
// Define the point_holder reducer
class point_holder
{
struct Monoid: cilk::monoid_base<point>
{
// reduce function does nothing
static void reduce (point *left, point *right) {}
};
private:
cilk::reducer<Monoid> imp_;
public:
point_holder() : imp_() {}
void set(int x, int y) {
point &p = imp_.view();
p.set(x, y);
}
bool is_valid() { return imp_.view().is_valid(); }
int x() { return imp_.view().x(); }
int y() { return imp_.view().y(); }
}; // class point_holder
サンプルプログラムで point_holder を使用する場合は、temp の宣言を reducer 型を使用した宣言に置換します。
置換前:
point temp; // temp is used when swapping two elements
置換後:
point_holder temp; // temp is used when swapping two elements
残りのプログラムを変更する必要はありません。
point_holder レデューサーは次のように動作します。
デフォルトのコンストラクターは、新しい point_holder が作成されるたびに (つまり、スチール後に temp が参照されるたびに)、新しいインスタンスを作成します。
reduce メソッドは何もしません。 temp は一時ストレージとして使用されるため、左側のインスタンスと右側のインスタンスをマージする必要はありません。
デフォルトのデストラクターがデストラクターを呼び出し、メモリーを解放します。
ループの各反復で生成されたローカルビューの値はマージされないため、cilk_for の内側で x() と y() を呼び出してローカルの値を取得しても問題ありません。
holder の reduce() 関数は何もしないので、デフォルトのコンストラクターは正確な単位元を提供する必要はありません。