ロケール間での配列/クラス/レコードの転送
典型的なN体シミュレーションでは、各エポックの終わりに、各ロケールは、世界の独自の部分(つまり、すべてのボディ)を残りのロケールと共有する必要があります。私はこれにローカルビューアプローチで取り組んでいます(つまり、on Loc
ステートメントを使用しています)。わからない奇妙な振る舞いに出くわしたので、もっと複雑なテストプログラムを作ることにしました。実験を再現するためのコードは次のとおりです。
proc log(args...?n) {
writeln("[locale = ", here.id, "] [", datetime.now(), "] => ", args);
}
const max: int = 50000;
record stuff {
var x1: int;
var x2: int;
proc init() {
this.x1 = here.id;
this.x2 = here.id;
}
}
class ctuff {
var x1: int;
var x2: int;
proc init() {
this.x1 = here.id;
this.x2 = here.id;
}
}
class wrapper {
// The point is that total size (in bytes) of data in `r`, `c` and `a` are the same here, because the record and the class hold two ints per index.
var r: [{1..max / 2}] stuff;
var c: [{1..max / 2}] owned ctuff?;
var a: [{1..max}] int;
proc init() {
this.a = here.id;
}
}
proc test() {
var wrappers: [LocaleSpace] owned wrapper?;
coforall loc in LocaleSpace {
on Locales[loc] {
wrappers[loc] = new owned wrapper();
}
}
// rest of the experiment further down.
}
ここでは、2つの興味深い動作が発生します。
1.データの移動
さて、の各インスタンスwrapper
の配列では、wrappers
そのロケールに住んでいる必要があります。具体的には、参照は、( wrappers
)ロケール0に住んでいるであろうが、内部データ(r
、c
、a
)それぞれのロケールに住んでいなければなりません。そのため、ロケール1からロケール3にいくつか移動しようとします。
on Locales[3] {
var timer: Timer;
timer.start();
var local_stuff = wrappers[1]!.r;
timer.stop();
log("get r from 1", timer.elapsed());
log(local_stuff);
}
on Locales[3] {
var timer: Timer;
timer.start();
var local_c = wrappers[1]!.c;
timer.stop();
log("get c from 1", timer.elapsed());
}
on Locales[3] {
var timer: Timer;
timer.start();
var local_a = wrappers[1]!.a;
timer.stop();
log("get a from 1", timer.elapsed());
}
驚いたことに、私のタイミングはそれを示しています
サイズ(
const max
)に関係なく、配列とレコードを送信する時間は一定であり、私には意味がありません。で確認したところchplvis
、GET
実際にはサイズが大きくなっていますが、時間は変わりません。クラスフィールドを送信する時間は時間とともに増加します。これは理にかなっていますが、非常に遅く、ここでどちらのケースを信頼するかわかりません。
2.ロケールを直接クエリします。
問題をわかりやすく説明するために、.locale.id
いくつかの変数のを直接クエリします。最初に、ロケール2に存在すると予想されるデータをロケール2からクエリします。
on Locales[2] {
var wrappers_ref = wrappers[2]!; // This is always 1 GET from 0, okay.
log("array",
wrappers_ref.a.locale.id,
wrappers_ref.a[1].locale.id
);
log("record",
wrappers_ref.r.locale.id,
wrappers_ref.r[1].locale.id,
wrappers_ref.r[1].x1.locale.id,
);
log("class",
wrappers_ref.c.locale.id,
wrappers_ref.c[1]!.locale.id,
wrappers_ref.c[1]!.x1.locale.id
);
}
そして結果は次のとおりです。
[locale = 2] [2020-12-26T19:36:26.834472] => (array, 2, 2)
[locale = 2] [2020-12-26T19:36:26.894779] => (record, 2, 2, 2)
[locale = 2] [2020-12-26T19:36:27.023112] => (class, 2, 2, 2)
これは予想されます。ただし、ロケール1で同じデータのロケールをクエリすると、次のようになります。
[locale = 1] [2020-12-26T19:34:28.509624] => (array, 2, 2)
[locale = 1] [2020-12-26T19:34:28.574125] => (record, 2, 2, 1)
[locale = 1] [2020-12-26T19:34:28.700481] => (class, 2, 2, 2)
ことを意味しているwrappers_ref.r[1].x1.locale.id
ことが明らかにロケール2にする必要がありますにもかかわらず、ロケール1に命を。私の唯一の推測は、.locale.id
実行されるまでに、データ(つまり.x
レコードの)はすでにクエリロケールに移動されているということです(1)。
したがって、全体として、実験の2番目の部分は、最初の部分には答えずに、2番目の質問につながります。
注:すべての実験は-nl 4
、chapel/chapel-gasnet
Dockerイメージで実行されます。
回答
良い観察です、私がいくつかの光を当てることができるかどうか見てみましょう。
最初の注意として、gasnet Dockerイメージで取得されるタイミングは、Chapelで意図されているように各ロケールを独自の計算ノードで実行するのではなく、ローカルシステムを使用して複数のノードにわたる実行をシミュレートするため、一粒の塩で取得する必要があります。その結果、分散メモリプログラムの開発には役立ちますが、パフォーマンス特性は実際のクラスターやスーパーコンピューターで実行する場合とは大きく異なる可能性があります。とはいえ、それでも、大まかなタイミングを取得する場合(たとえば、「これにははるかに長い時間がかかる」という観察結果)、またはCommDiagnosticsモジュールを使用して通信をカウントするchplvis
場合に役立ちます。
タイミングに関するあなたの観察に関して、私はまた、クラスの配列の場合がはるかに遅いことを観察し、私はいくつかの振る舞いを説明できると信じています:
まず、ノード間の通信は、のような式を使用して特徴付けることができることを理解することが重要alpha + beta*length
です。alpha
長さに関係なく、通信を実行するための基本的なコストを表すと考えてください。これは、ソフトウェアスタックを介して呼び出してネットワークにアクセスし、データをネットワークに配置し、反対側で受信し、ソフトウェアスタックを介してそこにあるアプリケーションに戻すコストを表します。アルファの正確な値は、通信のタイプ、ソフトウェアスタックの選択、物理ハードウェアなどの要因によって異なります。一方、beta
通信のバイト単位のコストを表すと考えてください。直感的には、通信の実装方法に応じて、ネットワークに配置するデータ、またはバッファリングまたはコピーするデータが増えるため、メッセージが長くなると必然的にコストが高くなります。
私の経験では、alpha
通常beta
、ほとんどのシステム構成での値が支配的です。これは、より長いデータ転送を自由に実行できるということではありませんが、実行時間の変動は、単一の転送と多数の転送を実行する場合よりも、長い転送と短い転送の方がはるかに小さい傾向があります。その結果、n
要素の1つの転送を実行するか、1つの要素を転送するかを選択する場合、n
ほとんどの場合、前者が必要になります。
タイミングを調査するためにCommDiagnostics
、次のように、時間指定されたコード部分をモジュールの呼び出しで囲みました。
resetCommDiagnostics();
startCommDiagnostics();
...code to time here...
stopCommDiagnostics();
printCommDiagnosticsTable();
そして、あなたがしたようにchplvis
、レコードの配列またはintの配列をローカライズするために必要な通信の数は、私が変化したmax
ときに一定であることがわかりました。たとえば、次のようになります。
ロケール | 取得する | execute_on |
---|---|---|
0 | 0 | 0 |
1 | 0 | 0 |
2 | 0 | 0 |
3 | 21 | 1 |
これは、実装に期待することと一致しています。値型の配列の場合、一定数の通信を実行して配列メタデータにアクセスし、1回のデータ転送で配列要素自体を通信して、オーバーヘッド(複数のalpha
コストを支払うことは避けてください)。
対照的に、クラスの配列をローカライズするための通信の数は、配列のサイズに比例することがわかりました。たとえば、のデフォルト値である50,000の場合max
、次のようになりました。
ロケール | 取得する | 置く | execute_on |
---|---|---|---|
0 | 0 | 0 | 0 |
1 | 0 | 0 | 0 |
2 | 0 | 0 | 0 |
3 | 25040 | 25000 | 1 |
この区別の理由c
は、owned
クラスの配列であり、一度に1つのクラス変数のみが特定のctuff
オブジェクトを「所有」できるという事実に関連していると思います。その結果、配列の要素をc
あるロケールから別のロケールにコピーする場合、レコードや整数の場合のように生データをコピーするだけでなく、要素ごとに所有権の転送も実行します。これには基本的に、リモート値をnil
ローカルクラス変数にコピーした後にに設定する必要があります。現在の実装では、これはリモートget
を使用してリモートクラス値をローカル値にコピーし、次にリモートput
を使用してリモート値をに設定するように見えますnil
。したがって、配列要素ごとにgetとputがあり、結果はOになります。 (n)前の場合のようにO(1)ではなく通信。追加の努力で、コンパイラにこのケースを最適化させることができる可能性がありますが、所有権の譲渡を実行する必要があるため、他のケースよりも常に高価になると思います。
私は、という仮説テストowned
クラスがあなたの変更による追加のオーバーヘッドが生じたctuff
ことからオブジェクトをowned
へのunmanaged
実装から任意の所有権の意味を削除し、。これを行うと、値の場合のように、一定数の通信が表示されます。
ロケール | 取得する | execute_on |
---|---|---|
0 | 0 | 0 |
1 | 0 | 0 |
2 | 0 | 0 |
3 | 21 | 1 |
これは、言語がクラス変数の所有権を管理する必要がなくなると、ポインター値を1回の転送で再度転送できるという事実を表していると思います。
これらのパフォーマンスノートに加えて、どちらを使用するかを選択するときは、クラスとレコードの重要なセマンティックの違いを理解することが重要です。クラスオブジェクトはヒープに割り当てられ、クラス変数は基本的にそのオブジェクトへの参照またはポインタです。したがって、クラス変数があるロケールから別のロケールにコピーされると、ポインターのみがコピーされ、元のオブジェクトは元の場所に残ります(良くも悪くも)。対照的に、レコード変数はオブジェクト自体を表し、「インプレース」で割り当てられていると考えることができます(たとえば、ローカル変数のスタック上)。レコード変数が1つのロケールから別のロケールにコピーされると、コピーされるのはオブジェクト自体(つまり、レコードのフィールドの値)であり、オブジェクト自体の新しいコピーになります。詳細については、このSOの質問を参照してください。
2番目の観察に移ると、あなたの解釈は正しいと思います。これは実装のバグである可能性があります(自信を持って理解するには、もう少し詳しく説明する必要があります)。具体的にwrappers_ref.r[1].x1
は、結果がローカル変数に格納されて評価されていること、および.locale.id
元のフィールドではなく結果を格納しているローカル変数にクエリが適用されていることは正しいと思います。私は、次のようref
に、フィールドに移動しlocale.id
、その参照を印刷することによって、この理論をテストしました。
ref x1loc = wrappers_ref.r[1].x1;
...wrappers_ref.c[1]!.x1.locale.id...
そしてそれは正しい結果を与えるようでした。また、私たちの理論が正しいことを示しているように見える生成されたコードも調べました。実装がこのように動作する必要があるとは思いませんが、自信を持つ前にもう少し考える必要があります。ChapelのGitHubの問題ページでこれに対するバグを開きたい場合は、そこでさらに議論するために、それをいただければ幸いです。