1

アプリケーションでマルチコア計算を使用したいと考えています。openMP (C++) を使用したサンプル アプリケーションの開発を開始します。

開始すると、マルチコア計算がシリアルよりも速くないことがわかりました(マルチコア計算でさえ、場合によってはシリアル計算よりも遅くなります):

./openmp_test

シリアル。合計: 1.77544e+08 時間: 21.84

削減、2 スレッド。合計: 1.77544e+08 時間: 21.65

2 つのセクション。合計: 1.77544e+08 時間: 60.65

次に考えたのは、CPU のコアで 2 つのスレッドをテストするためのboost::thread アプリケーションを作成することでした。結果:

./boost_thread_test

シリアル。合計: 1.42146e+09 時間: 179.64

2 つのブースト スレッド。合計: 1.42146e+09 時間: 493.34

内部にCore i3 CPUを搭載したopenSuSe(x64)搭載のラップトップを使用しています。

マルチスレッドのパフォーマンスが悪いのはなぜですか?

4

2 に答える 2

3

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に並べ替えないという保証がない可能性があります。 sum2pad。もしそうなら、それらは構造に配置することができます。

問題は基本的にスレッドの場合と同じです。クラスデータメンバーは次のことを行います。

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ディレクティブを使用したコードと同じ速度で実行されます。別の可能な解決策は、ローカル変数に累積し、ループの終了後に割り当てることですsum1sum2

#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++クラスを使用して速度にどのような影響があるかを確認してください。

于 2012-12-10T12:18:42.193 に答える
0

コメントとして入れるには長すぎます

との実装には非常に奇妙なことがsinありcosます。

(編集:もちろん、とは関係ありませんが、配列へのアクセスパターンとsincosaは関係ありません)。

(編集2:また多数の冗長な呼び出しが排除されます。関数では、コンパイラーはループ不変呼び出しをループの内外に移動しますが、メソッド内では移動しません。したがって、これがパフォーマンスの違いを説明します。なぜコンパイラが異なることをするのかという質問を開く時が来ました:)sincossingle_threadsincosCalc::run

次の変更がある場合とない場合のプログラムを比較します。

シングルスレッドバージョンはほぼ同じ時間(約12秒)実行されますが、元のマルチスレッドバージョンは約18秒(つまりシングルスレッドバージョンよりも遅い)実行されますが、変更されたマルチスレッドバージョンは約7秒間実行されます秒(niter == 1000)。

--- thread-smp-orig.cxx        2012-12-10 12:40:03.547640307 +0200
+++ thread-smp.cxx        2012-12-10 12:37:27.990650712 +0200
@@ -26,11 +26,13 @@ public:
         double x;
         for (int i = start; i < end; i++)
         {
+            double sai = sin(a[i]);
             for (int j = 0; j < niter; j++)
             {
+                double caj = cos(a[j]);
                 for (int k = 0; k < niter; k++)
                 {
-                    x = sin(a[i]) * cos(a[j]) * sin(a[k]);
+                    x = sai * caj * sin(a[k]);
                     sum += x;
                 }
             }
@@ -48,11 +50,13 @@ double single_thread(double a[], const i
     double x;
     for (int i = 0; i < niter; i++)
     {
+        double sai = sin(a[i]);
         for (int j = 0; j < niter; j++)
         {
+            double caj = cos(a[j]);
             for (int k = 0; k < niter; k++)
             {
-                x = sin(a[i]) * cos(a[j]) * sin(a[k]);
+                x = sai * caj * sin(a[k]);
                 sum += x;
             }
         }
于 2012-12-10T10:51:49.027 に答える