22

プログラムの一部を OpenMP* で並列化したことが何度かありましたが、最終的には、優れたスケーラビリティーにもかかわらず、シングルスレッドのケースのパフォーマンスが低いために、予測されたスピードアップのほとんどが失われたことに気付きました (シリアル版)。

この動作についてウェブ上で見られる通常の説明は、マルチスレッドの場合、コンパイラによって生成されたコードが悪化する可能性があるというものです。とにかく、アセンブリが悪化する理由を説明する参照をどこにも見つけることができません。

それで、私がそこにいるコンパイラの人たちに尋ねたいのは:

マルチスレッドによってコンパイラの最適化が抑制されることはありますか? その場合、パフォーマンスはどのように影響を受ける可能性がありますか?

質問を絞り込むのに役立つ場合、私は主に高性能コンピューティングに興味があります。

免責事項:コメントに記載されているように、以下の回答の一部は、質問が提起された時点でコンパイラが最適化を処理する方法について簡単に説明しているため、将来的に時代遅れになる可能性があります。

4

4 に答える 4

6

OMP の明示的なプラグマを除いて、コンパイラは、コードが複数のスレッドで実行できることを認識していません。したがって、そのコードの効率を上げたり下げたりすることはできません。

これは、C++ では重大な結果をもたらします。これは、ライブラリの作成者にとって特に問題であり、スレッド化を使用するプログラムでコードが使用されるかどうかを前もって合理的に推測することはできません。一般的な C ランタイムおよび標準 C++ ライブラリの実装のソースを読むと、非常によくわかります。このようなコードは、スレッドで使用されたときにコードが正しく動作することを保証するために、いたるところに小さなロックが散りばめられている傾向があります。実際にそのコードをスレッド化された方法で使用していなくても、これには料金がかかります。良い例は std::shared_ptr<> です。スマート ポインターが 1 つのスレッドでしか使用されない場合でも、参照カウントのアトミックな更新に対して料金が発生します。また、標準では非アトミックな更新を要求する方法が提供されていないため、機能を追加する提案は却下されました。

また、他の方法でも非常に有害です。独自のコードがスレッドセーフであることを保証するためにコンパイラーができることは何もありません。スレッドセーフにするかどうかは完全にあなた次第です。実行するのは難しく、これは常に微妙で非常に診断が難しい方法でうまくいかない.

簡単に解決できない大きな問題。そうでなければ、誰もがプログラマーになれるかもしれません ;)

于 2013-05-29T11:36:35.623 に答える
6

この回答は理由を十分に説明していると思いますが、ここで少し拡張します。

ただし、以前は、次のgcc 4.8 のドキュメントが-fopenmpあります。

-fopenmp
C/C++ では OpenMP ディレクティブ #pragma omp および Fortran では !$omp の処理を​​有効にします。-fopenmp を指定すると、コンパイラは OpenMP Application Program Interface v3.0 http://www.openmp.org/に従って並列コードを生成します。このオプションは -pthread を意味するため、-pthread をサポートするターゲットでのみサポートされます。

機能の無効化を指定していないことに注意してください。実際、gcc が最適化を無効にする理由はありません。

ただし、1 スレッドの openmp に openmp がない場合と比べてオーバーヘッドがある理由は、コンパイラがコードを変換し、関数を追加して、n>1 スレッドの openmp の場合に備えられるようにする必要があるためです。それでは、簡単な例を考えてみましょう。

int *b = ...
int *c = ...
int a = 0;

#omp parallel for reduction(+:a)
for (i = 0; i < 100; ++i)
    a += b[i] + c[i];

このコードは、次のように変換する必要があります。

struct __omp_func1_data
{
    int start;
    int end;
    int *b;
    int *c;
    int a;
};

void *__omp_func1(void *data)
{
    struct __omp_func1_data *d = data;
    int i;

    d->a = 0;
    for (i = d->start; i < d->end; ++i)
        d->a += d->b[i] + d->c[i];

    return NULL;
}

...
for (t = 1; t < nthreads; ++t)
    /* create_thread with __omp_func1 function */
/* for master thread, don't create a thread */
struct master_data md = {
    .start = /*...*/,
    .end = /*...*/
    .b = b,
    .c = c
};

__omp_func1(&md);
a += md.a;
for (t = 1; t < nthreads; ++t)
{
    /* join with thread */
    /* add thread_data->a to a */
}

これを で実行するとnthreads==1、コードは実質的に次のように縮小されます。

struct __omp_func1_data
{
    int start;
    int end;
    int *b;
    int *c;
    int a;
};

void *__omp_func1(void *data)
{
    struct __omp_func1_data *d = data;
    int i;

    d->a = 0;
    for (i = d->start; i < d->end; ++i)
        d->a += d->b[i] + d->c[i];

    return NULL;
}

...
struct master_data md = {
    .start = 0,
    .end = 100
    .b = b,
    .c = c
};

__omp_func1(&md);
a += md.a;

では、openmp なしのバージョンとシングルスレッドの openmp バージョンの違いは何ですか?

1 つの違いは、追加のグルー コードがあることです。openmp によって作成された関数に渡す必要がある変数は、1 つの引数を形成するためにまとめる必要があります。そのため、関数呼び出しの準備 (および後でデータを取得する) のオーバーヘッドが発生します。

しかし、もっと重要なことは、コードがもはや一体ではないということです。機能間の最適化はまだそれほど進んでおらず、ほとんどの最適化は各機能内で行われています。関数が小さいということは、最適化する可能性が小さいことを意味します。


-fopenmpこの回答を締めくくるために、 が のオプションにどのように影響するかを正確に示したいと思いgccます。(注:私は現在古いコンピューターを使用しているため、gcc 4.4.3 を使用しています)

実行gcc -Q -v some_file.cすると、次の(関連する)出力が得られます。

GGC heuristics: --param ggc-min-expand=98 --param ggc-min-heapsize=128106
options passed:  -v a.c -D_FORTIFY_SOURCE=2 -mtune=generic -march=i486
 -fstack-protector
options enabled:  -falign-loops -fargument-alias -fauto-inc-dec
 -fbranch-count-reg -fcommon -fdwarf2-cfi-asm -fearly-inlining
 -feliminate-unused-debug-types -ffunction-cse -fgcse-lm -fident
 -finline-functions-called-once -fira-share-save-slots
 -fira-share-spill-slots -fivopts -fkeep-static-consts -fleading-underscore
 -fmath-errno -fmerge-debug-strings -fmove-loop-invariants
 -fpcc-struct-return -fpeephole -fsched-interblock -fsched-spec
 -fsched-stalled-insns-dep -fsigned-zeros -fsplit-ivs-in-unroller
 -fstack-protector -ftrapping-math -ftree-cselim -ftree-loop-im
 -ftree-loop-ivcanon -ftree-loop-optimize -ftree-parallelize-loops=
 -ftree-reassoc -ftree-scev-cprop -ftree-switch-conversion
 -ftree-vect-loop-version -funit-at-a-time -fvar-tracking -fvect-cost-model
 -fzero-initialized-in-bss -m32 -m80387 -m96bit-long-double
 -maccumulate-outgoing-args -malign-stringops -mfancy-math-387
 -mfp-ret-in-387 -mfused-madd -mglibc -mieee-fp -mno-red-zone -mno-sse4
 -mpush-args -msahf -mtls-direct-seg-refs

実行すると、次のgcc -Q -v -fopenmp some_file.c(関連する)出力が得られます。

GGC heuristics: --param ggc-min-expand=98 --param ggc-min-heapsize=128106
options passed:  -v -D_REENTRANT a.c -D_FORTIFY_SOURCE=2 -mtune=generic
 -march=i486 -fopenmp -fstack-protector
options enabled:  -falign-loops -fargument-alias -fauto-inc-dec
 -fbranch-count-reg -fcommon -fdwarf2-cfi-asm -fearly-inlining
 -feliminate-unused-debug-types -ffunction-cse -fgcse-lm -fident
 -finline-functions-called-once -fira-share-save-slots
 -fira-share-spill-slots -fivopts -fkeep-static-consts -fleading-underscore
 -fmath-errno -fmerge-debug-strings -fmove-loop-invariants
 -fpcc-struct-return -fpeephole -fsched-interblock -fsched-spec
 -fsched-stalled-insns-dep -fsigned-zeros -fsplit-ivs-in-unroller
 -fstack-protector -ftrapping-math -ftree-cselim -ftree-loop-im
 -ftree-loop-ivcanon -ftree-loop-optimize -ftree-parallelize-loops=
 -ftree-reassoc -ftree-scev-cprop -ftree-switch-conversion
 -ftree-vect-loop-version -funit-at-a-time -fvar-tracking -fvect-cost-model
 -fzero-initialized-in-bss -m32 -m80387 -m96bit-long-double
 -maccumulate-outgoing-args -malign-stringops -mfancy-math-387
 -mfp-ret-in-387 -mfused-madd -mglibc -mieee-fp -mno-red-zone -mno-sse4
 -mpush-args -msahf -mtls-direct-seg-refs

diff を取ると、唯一の違いは、-fopenmp-D_REENTRANT定義されていること (そしてもちろん-fopenmp有効になっていること) であることがわかります。したがって、gcc が悪いコードを生成することはありませんのでご安心ください。スレッド数が 1 よりも大きく、オーバーヘッドが発生する場合の準備コードを追加する必要があるだけです。


更新:最適化を有効にしてこれをテストする必要がありました。とにかく、gcc 4.7.3 では、同じコマンドの出力を追加-O3すると、同じ違いが得られます。したがって、 を使用しても、-O3最適化が無効になることはありません。

于 2013-05-29T16:09:44.710 に答える
3

かなり大雑把ではありますが、それは良い質問です。専門家からの回答を楽しみにしています。@JimCownie は、次のディスカッションでこれについて良いコメントをしたと思いますomp_set_num_threads(1) が openmp なしよりも遅い理由

自動ベクトル化と並列化はしばしば問題になると思います。MSVC 2012 で自動並列化をオンにすると (自動ベクトル化はデフォルトでオンになっています)、うまく混ざり合わないようです。OpenMP を使用すると、MSVC の自動ベクトル化が無効になるようです。OpenMP と自動ベクトル化を使用した GCC にも同じことが当てはまるかもしれませんが、よくわかりません。

とにかく、コンパイラーの自動ベクトル化はあまり信用していません。理由の 1 つは、キャリー ループの依存性とスカラー コードを排除するためにループ展開を行うかどうかわからないことです。このため、私はこれらのことを自分でやろうとしています。自分でベクトル化を行い (Agner Fog のベクトル クラスを使用)、自分でループを展開します。これを手作業で行うことで、TLP (例: OpenMP を使用)、ILP (例: ループ アンローリングでデータの依存関係を削除することにより)、および SIMD (明示的な SSE/AVX コードを使用する場合) のすべての並列処理を最大化する自信がつきました。

于 2013-05-29T09:37:35.077 に答える
3

上記には多くの良い情報がありますが、適切な答えは、OpenMP をコンパイルするときにいくつかの最適化をオフにする必要があるということです。gcc などの一部のコンパイラは、これを行いません。

この回答の最後にあるサンプル プログラムは、4 つの重複しない整数範囲で値 81 を検索しています。常にその値を見つける必要があります。ただし、少なくとも 4.7.2 までのすべての gcc バージョンでは、プログラムが正しい応答で終了しないことがあります。自分で確認するには、次のようにします。

  • プログラムをファイルにコピーするparsearch.c
  • でコンパイルしますgcc -fopenmp -O2 parsearch.c
  • で実行しますOMP_NUM_THREADS=2 ./a.out
  • さらに数回 (おそらく 10 回) 実行すると、2 つの異なる回答が表示されます。

または、なしでコンパイル-O0して、結果が常に正しいことを確認できます。

プログラムに競合状態がないことを考えると、コンパイラのこの動作-O2は正しくありません。

この動作はグローバル変数によるものですglobFoundparallel for予想される実行では、4 つのスレッドのうちの 1 つだけがその変数に書き込むことを確信してください。OpenMP セマンティクスでは、グローバル (共有) 変数が 1 つのスレッドのみによって書き込まれる場合、並列 for の後のグローバル変数の値は、その単一のスレッドによって書き込まれた値であると定義されています。グローバル変数を介したスレッド間の通信はなく、競合状態が発生するため許可されません。

コンパイラの最適化が行う-O2ことは、ループ内のグローバル変数への書き込みはコストがかかると推定されているため、それをレジスタにキャッシュするということです。これは関数finditで発生し、最適化後は次のようになります。

int tempo = globFound ;
for ( ... ) {
    if ( ...) {
        tempo = i;
    }
globFound = tempo;

しかし、この「最適化された」コードでは、すべてのスレッドが読み取りと書き込みglobFoundを行い、コンパイラ自体によって競合状態が発生します。

コンパイラの最適化では、並列実行を認識する必要があります。これに関する優れた資料が Hans-J によって公開されています。Boehm、メモリの一貫性の一般的なトピックの下。

#include <stdio.h>
#define BIGVAL  (100 * 1000 * 1000)

int globFound ;

void findit( int from, int to )
{
    int i ;

    for( i = from ; i < to ; i++ ) {
        if( i*i == 81L ) {
            globFound = i ;
        }
    }
}

int main( int argc, char *argv )
{
    int p ;

    globFound = -1 ;

    #pragma omp parallel for
    for( p = 0 ; p < 4 ; p++ ) {
        findit( p * BIGVAL, (p+1) * BIGVAL ) ;
    }
    if( globFound == -1 ) {
        printf( ">>>>NO 81 TODAY<<<<\n\n" ) ;
    } else {
        printf( "Found! N = %d\n\n", globFound ) ;
    }
    return 0 ;
}
于 2014-04-29T20:19:29.693 に答える