7

みなさん、良い一日を!

私は分子動力学シミュレーションを行っていますが、最近、それを並行して実装しようと試み始めました。一見するとすべてが単純に見えます: 最も時間のかかるループの前に #pragma omp parallel for ディレクティブを記述します。しかし、たまたま、これらのループ内の関数は配列、または正確には、粒子システムに関するすべての情報とこのシステムで動作する関数を含むクラスのオブジェクトに属する配列で動作するため、 #最も時間のかかるループの 1 つの前に pragma ディレクティブを追加すると、2 コア 4 スレッド プロセッサが完全にロードされたという事実にもかかわらず、計算時間は実際には数倍増加しました。

これを整理するために、別のより単純なプログラムを作成しました。このテスト プログラムは、2 つの同一のループを実行します。1 つは並列で、もう 1 つはシリアルです。これらのループの両方を実行するのにかかる時間が測定されます。結果は私を驚かせました.最初のループが並列で計算されるときはいつでも、その計算時間はシリアルモードと比較して減少しました (それぞれ 1500 および 6000 ミリ秒) が、2 番目のループの計算時間は大幅に増加しました (シリアルの 6000 に対して 15 000)。

private() 句と firstprivate() 句を使用しようとしましたが、結果は同じでした。とにかく、並列領域の前に定義および初期化されたすべての変数を自動的に共有するべきではありませんか? 2 番目のループの計算時間は、別のベクトル vec2 で実行すると通常に戻りますが、反復ごとに新しいベクトルを作成することは明らかにオプションではありません。また、vec1 の実際の更新を #pragma omp クリティカル エリアに入れようとしましたが、それもうまくいきませんでした。どちらも Shared(vec1) 句の追加に役立ちませんでした。

私の誤りを指摘し、適切な方法を示していただければ幸いです。

その private(i) をコードに入れる必要がありますか?

このテストプログラムは次のとおりです。

#include "stdafx.h"
#include <omp.h>
#include <array>
#include <time.h>
#include <vector>
#include <iostream>
#include <Windows.h>
using namespace std;
#define N1  1000
#define N2  4000
#define dim 1000

int main(){
    vector<int>res1,res2;
    vector<double>vec1(dim),vec2(N1);
    clock_t t, tt;
    int k=0;
    for( k = 0; k<dim; k++){
        vec1[k]=1;
    }

    t = clock();

    #pragma omp parallel 
        {
        double temp; 
        int i,j,k;
        #pragma omp for private(i)
            for( i = 0; i<N1; i++){
                for(j = 0; j<N2; j++){  
                    for( k = 0; k<dim; k++){
                        temp+= j;
                    }
                }
                vec1[i]+=temp;
                temp = 0;
            }
        }
    tt = clock();
    cout<<tt-t<<endl;
    for(int k = 0; k<dim; k++){
        vec1[k]=1;
    }
    t = clock();
                for(int g = 0; g<N1; g++){
        for(int h = 0; h<N2; h++){
            for(int y = 0; y<dim; y++){
                vec1[g]+=h; 
            }
        }
    }
    tt = clock();
    cout<<tt-t<<endl;
    getchar();
}

お時間をいただきありがとうございます!

PS 私は Visual Studio 2012 を使用しています。プロセッサは Intel Core i3-2370M です。私のアセンブリファイルは2つの部分に分かれています:

http://pastebin.com/suXn35xj

http://pastebin.com/EJAVabhF

4

1 に答える 1

9

おめでとう!Microsoft の厚意により、さらに別の悪い OpenMP 実装が公開されました。私の最初の理論は、この問題は Sandy Bridge およびそれ以降の Intel CPU の分割された L3 キャッシュに起因するというものでした。しかし、ベクトルの前半だけで 2 番目のループを実行した結果は、その理論を裏付けるものではありませんでした。次に、OpenMP が有効になったときにトリガーされるコード ジェネレーター内の何かでなければなりません。アセンブリ出力はこれを確認します。

基本的に、OpenMP を有効にしてコンパイルすると、コンパイラはシリアル ループを最適化しません。失速はここからです。問題の一部は、2 番目のループを最初のループと同一にしないことによって、自分自身が導入したものでもあります。最初のループでは、中間値を一時変数に蓄積します。これは、コンパイラが変数を登録するために最適化します。2 番目のケースでは、operator[]反復ごとに呼び出します。OpenMP を有効にせずにコンパイルすると、コード オプティマイザは 2 番目のループを最初のループと非常に似たものに変換するため、両方のループでほぼ同じ実行時間が得られます。

OpenMP を有効にすると、コード オプティマイザは 2 番目のループを最適化せず、実行速度が大幅に低下します。コードがその前に並列ブロックを実行するという事実は、速度低下とは何の関係もありません。私の推測では、コード オプティマイザはvec1OpenMPparallel領域の範囲外にあるという事実を把握できないため、共有変数として扱われなくなり、ループを最適化できるようになります。Visual Studio 2010 のコード ジェネレーターは、OpenMP が有効になっていても 2 番目のループを最適化できるため、明らかにこれは Visual Studio 2012 で導入された "機能" です。

考えられる解決策の 1 つは、Visual Studio 2010 に移行することです。もう 1 つの (私は VS2012 を持っていないため、仮定上の) 解決策は、2 番目のループを関数に抽出し、参照によってベクトルを渡すことです。うまくいけば、コンパイラは別の関数でコードを最適化するのに十分賢くなります。

これは非常に悪い傾向です。Microsoft は、Visual C++ で OpenMP をサポートすることを事実上断念しました。それらの実装は依然として (ほぼ) OpenMP 2.0 のみに準拠しており (したがって、明示的なタスクやその他の OpenMP 3.0+ の利点はありません)、このようなバグによって状況が改善されることはありません。別の OpenMP 対応コンパイラ (Intel C/C++ コンパイラ、GCC、Microsoft 以外のもの) に切り替えるか、Intel Threading Building Blocks などの他のコンパイラに依存しないスレッド化パラダイムに切り替えることをお勧めします。Microsoft は明らかに .NET 用の並列ライブラリを推し進めており、すべての開発はそこに向けられています。


大きな脂肪の警告

clock()壁時計の経過時間を測定するために使用しないでください。これは、Windows でのみ期待どおりに機能します。ほとんどの Unix システム (Linux を含む) では、実際には、プロセスが作成されてからプロセス内のすべてのスレッドによって消費された CPU 時間の合計clock()が返されます。これは、経過した実時間の数倍の値 (プログラムが多くのビジー スレッドで実行されている場合) または実時間の数倍短い値 (プログラムがスリープ状態または IO イベントを待機している場合) を返す可能性があることを意味します。測定値)。代わりに、OpenMP プログラムでは、ポータブル タイマー関数を使用する必要があります。clock()omp_get_wtime()

于 2012-12-17T15:11:29.090 に答える