OpenMPに基づくコードとに基づくコードの両方sections
が偽共有boost::thread
の犠牲になっている可能性があります。一時的なロードとストアは、オペランドを直接操作するのではなく、キャッシュライン全体を操作するため、偽共有が発生します。たとえば、次のステートメント:
sum = sum + value;
その結果、メモリから読み取られ、更新されてから書き戻されるだけでなく、キャッシュラインsum
が読み取られてから書き戻される、メモリの小さなセクション全体が得られます。最近のx86CPUのキャッシュラインは通常64バイトのオーダーです。これは、の値がメモリからロード/メモリに格納されるだけでなく、その周囲の56バイトも意味します。キャッシュラインも常に64の倍数のアドレスで始まります。コードへの影響は何ですか?sum
OpenMPセクションのコードには次のものがあります。
double sum1;
double sum2;
...
// one section operates on sum1
...
// one section operates on sum2
...
sum1
およびsum2
は親関数のスタックにありますomp_sections
(補足-omp_
プレフィックスはOpenMPランタイムライブラリ内の関数用に予約されています。独自の関数に名前を付けるために使用しないでください!)。2倍にsum1
なりsum2
、8バイト境界に配置され、合計で16バイトかかります。両方が同じキャッシュライン内に入る確率は7/8または87.5%です。最初のスレッドが更新したいときに何が起こるかsum1
は次のとおりです。
- 保持しているキャッシュラインを読み取ります
sum1
- の値を更新します
sum1
- キャッシュラインの内容が変更されたため、キャッシュ内で無効にする必要があることを他のすべてのコアに通知します
最後の部分は非常に重要です-それはキャッシュコヒーレンスとして知られているものの一部です。sum1
とは同じキャッシュラインに入る可能性が高いためsum2
、秒スレッドを実行するコアは、キャッシュを無効にして、下位のメモリ階層レベルから(たとえば、共有の最終レベルのキャッシュまたはメインメモリから)リロードする必要があります。2番目のスレッドが。の値を変更する場合も、まったく同じことが起こりますsum2
。
考えられる解決策の1つはreduction
、OpenMPワークシェアリングディレクティブを使用する場合と同じように句を使用することfor
です。
double sum;
#pragma omp parallel sections reduction(+:sum) num_threads(2)
{
...
}
別の可能な解決策は、2つの値の間にいくつかのパディングを挿入して、それらを複数のキャッシュラインから離すことです。
double sum1;
char pad[64];
double sum2;
C ++標準がローカル変数をスタックに配置する方法を保証するかどうかはわかりません。つまり、コンパイラが変数の配置を「最適化」せず、次のようsum1
に並べ替えないという保証がない可能性があります。 sum2
、pad
。もしそうなら、それらは構造に配置することができます。
問題は基本的にスレッドの場合と同じです。クラスデータメンバーは次のことを行います。
double *a; // 4 bytes on x86, 8 bytes on x64
int niter; // 4 bytes
int start; // 4 bytes
int end; // 4 bytes
// 4 bytes padding on x64 because doubles must be aligned
double sum; // 8 bytes
クラスデータメンバーは、x86で24バイト、x64で32バイト(64ビットモードではx86)を使用します。これは、2つのクラスインスタンスが同じキャッシュラインに収まるか、1つを共有する可能性が高いことを意味します。sum
ここでも、少なくとも32バイトのサイズの後にパディングデータメンバーを追加できます。
class Calc
{
private:
double *a;
int niter;
int start;
int end;
double sum;
char pad[32];
...
};
private
句によって作成された暗黙のプライベートコピーを含む変数はreduction
、個々のスレッドのスタックに存在する可能性が高いため、キャッシュラインが1行以上離れているため、偽共有が発生せず、コードが並行して高速に実行されることに注意してください。
編集:ほとんどのコンパイラが最適化フェーズで未使用の変数を削除することを忘れました。OpenMPセクションの場合、パディングはほとんど最適化されています。これは、代わりにアライメント属性を適用することで解決できます(警告:GCC固有の可能性があります)。
double sum1 __attribute__((aligned(64))) = 0;
double sum2 __attribute__((aligned(64))) = 0;
sum1
これにより偽共有は削除されますが、とsum2
は共有変数であるため、ほとんどのコンパイラがレジスタ最適化を使用できなくなります。したがって、リダクションを使用するバージョンよりも低速になります。私のテストシステムでは、両方の変数をキャッシュラインの境界に揃えると、シリアル実行時間が20秒の場合、実行時間が56秒から30秒に短縮されます。これは、OpenMPコンストラクトが一部のコンパイラー最適化を台無しにし、パラレルコードの実行がシリアルコードよりもはるかに遅くなる可能性があることを示しているだけなので、注意が必要です。
両方の変数を作成できますlastprivate
。これにより、コンパイラーはそれらに対してレジスターの最適化を実行できます。
#pragma omp parallel sections num_threads(2) lastprivate(sum1,sum2)
この変更により、セクションコードは、worksharingディレクティブを使用したコードと同じ速度で実行されます。別の可能な解決策は、ローカル変数に累積し、ループの終了後に割り当てることですsum1
。sum2
#pragma omp section
{
double s = 0;
for (int i = 0; i < niter / 2; i++)
{
for (int j = 0; j < niter; j++)
{
for (int k = 0; k < niter; k++)
{
double x = sin(a[i]) * cos(a[j]) * sin(a[k]);
s += x;
}
}
}
sum1 = s;
}
// Same for the other section
これは本質的にと同等threadprivate(sum1)
です。
残念ながら、私はboost
インストールしていないので、スレッドコードをテストできません。soを使用して計算全体を実行Calc::run()
し、C++クラスを使用して速度にどのような影響があるかを確認してください。