Piを並行して検討します。 パート1



この一連の投稿では、多かれ少なかれ関連する並列プログラミングテクノロジ(ネイティブストリーム、OpenMP、TBB、MPI、CUDA、OpenCL、OpenACC、Chapelを使用して、1つの単純な問題を解決しようとします。 -キー。



すべてが1つの投稿に収まるように(いくつかの部分で)、一般に注意によって集合的に捕捉できるように、問題は慎重に選択されました。 したがって、前述のテクノロジーの多くの側面、そして最も重要なことには、現代の超並列システムの実際のプログラミング中に生じる困難は、舞台裏のままになります。 また、「GPU vs CPU」などのベンチマークとして上記および例を使用しないでください。 唯一の目的は、「仕組み」を示すことです。 この記事は、並列プログラミングをあまり知らない人向けに設計されています。 カットの下に多くのコードがあります。 実際、数値積分によりPiを考慮します 画像



ソースコードはgithub.com/undertherain/piでも入手できます(投稿の次の部分が書き込まれると、補充されます)。



シリアルバージョン。



シリアルバージョンを作成することから始めましょう。 つまり 従来の中央処理装置の1つのコアを使用するもの。 数値積分の最も単純な方法である長方形法を採用し、C言語でコーディングします(一般的に、C \ C ++ surzhikをいくつかの理由で使用します。

特定の数のcntSteps長方形を宣言し、その下に積分の下で領域を分割し、基底を計算します。



step = 1./static_cast<double>(cntSteps);
      
      







領域全体。各長方形の関数の値をカウントし、ベースで乗算します。



 for (unsigned long i=0; i<cntSteps; i++) { x = ( i + .5 ) *step; sum = sum + 4.0/(1.+ x*x); }
      
      







ただし、「ブラケットの外側」の方が基本ステップを掛ける、つまり サイクルのために-それは基本的にそれです。



完全なプログラムコードは次のとおりです。



 #include <iostream> #include <iomanip> #include <sys/times.h> #include <cmath> int main(int argc, char** argv) { const unsigned long cntSteps=500000000; /* # of rectangles */ double step; const double PI25DT = 3.141592653589793238462643; //reference Pi value double pi=0; double sum=0.0; double x; std::cout<< "calculating pi on CPU single-threaded\n"; //  clock_t clockStart, clockStop; tms tmsStart, tmsStop; clockStart = times(&tmsStart); //      step = 1./static_cast<double>(cntSteps); for (unsigned long i=0; i<cntSteps; i++) { x = (i + .5)*step; sum = sum + 4.0/(1.+ x*x); } pi = sum*step; //      clockStop = times(&tmsStop); std::cout << "The value of PI is " << pi << " Error is " << fabs(pi - PI25DT) << "\n"; std::cout << "The time to calculate PI was " ; double secs= (clockStop - clockStart)/static_cast<double>(sysconf(_SC_CLK_TCK)); std::cout << secs << " seconds\n"; return 0; }
      
      







http://dumpz.org/195276/でコピーします



ネイティブスレッド



通常のユーザーが最もアクセスしやすい並列アーキテクチャは、通常のマルチコアプロセッサ(1つのコアを持つプロセッサを見つけるのが難しいようです)または同じマザーボード上の複数のプロセッサです。 原則として、単一のコアで複数のスレッドを並行して実行できます。このモードは、擬似並列または競合と呼ばれます。 カーネルはプロセス(プロセスはおおよそメモリ内のプログラムです)を切り替え、それぞれにタイムスライスを割り当てます。 原則として、このような実行モードは、通常の「ホーム」プロセッサではなく、特殊なマルチスレッドプロセッサではなく、メモリレイテンシを非表示にすることにより、すでにパフォーマンスを向上させることができます。 私たちの場合、スレッドを切り替えるオーバーヘッドが原因で、過剰な数のスレッドがスローダウンします。



プロセッサ上の複数のコアを一度に使用する最も「歴史的な」方法は、プログラムのより便利な作成のためだけに、競争のための真の並列プロセッサのはるか以前に存在したオペレーティングシステムのスレッドメカニズムです。 プログラマーの観点からは、異なるコアまたはプロセッサーで実行されている並列スレッドが同じアドレス空間を見ることが重要です。つまり、スレッド間で明示的にデータを転送する必要はありません。 しかし、突然異なるスレッドが同じ変数を読み書きする場合は、同期に注意する必要があります。



さて、コードに近づきましょう。C言語の観点から見ると、ストリームは特定のプロトタイプを満たす通常の関数またはクラスメソッドです。 static void * worker void * ptrArgs と呼びましょう。引数により、ストリームに引数を渡すのに必要な数のフィールドを作成できる構造体へのポインタを取得します。 この場合、積分を考慮する範囲をストリーム関数に指示します。 ストリーム関数の本体には、sobsnoとパラメーターで渡された範囲でカウントするsobsnoの既知のサイクルがあります。



 for (long long i=args->left; i<args->right; i++) { x = (i + .5)*step; sum = sum + 4.0/(1.+ x*x); }
      
      







シリアル番号に基づいて事前に計算する各ストリームの統合間隔。 スレッドの1つが先にその部分をカウントした場合、対応するコアはアイドル状態になります。 生産性が失われます。 間隔を多数の小さなセクションに分割し、スレッドがジョブを実行するときにスレッドに分散することが理想的です。 しかし、今のところは、そのままにしておきます。



 arrArgsThread[idThread].left = idThread*cntStepsPerThread; arrArgsThread[idThread].right = (idThread+1)*cntStepsPerThread;
      
      







別のスレッドによる実行では、POSIXの場合(たとえばLinuxの場合)またはWindowsの場合はWin32 APIからの同様の呼び出しになりますが、関数はpthread_createシステムコールを介して起動されます。



各ストリームの結果を共通変数pi + = sum * stepに追加します。 (私たちは共通のアドレス空間にいることに注意してください)。



2つのスレッドが同時に1つのセルを変更してもメモリが劣化しないように、ある時点で1つのスレッドのみが変数piにアクセスできるようにする必要があります。 いわゆる「クリティカルセクション」を作成します。 これを行うには、ミューテックス(相互排除という言葉から)と呼ばれる特別なオペレーティングシステムメカニズムを使用できます.1つのスレッドがミューテックスをブロックすると、他のスレッドは最初のスレッドが解放するまで待機します(ミューテックス自体を取得しようとします)。



 pthread_mutex_lock(&mutexReduction); pi += sum*step; pthread_mutex_unlock(&mutexReduction);
      
      





合計は次のようになります。



 #include <iostream> #include <iomanip> #include <cmath> #include <cstdlib> #include <pthread.h> #include <sys/times.h> #define cntThreads 4 pthread_mutex_t mutexReduction; double pi=0.; //      struct ArgsThread { long long left,right; double step; }; //  static void *worker(void *ptrArgs) { ArgsThread * args = reinterpret_cast<ArgsThread *>(ptrArgs); double x; double sum=0.; double step=args->step; for (long long i=args->left; i<args->right; i++) { x = (i + .5)*step; sum = sum + 4.0/(1.+ x*x); } pthread_mutex_lock(&mutexReduction); pi += sum*step; pthread_mutex_unlock(&mutexReduction); return NULL; } int main(int argc, char** argv) { const unsigned long num_steps=500000000; const double PI25DT = 3.141592653589793238462643; pthread_t threads[cntThreads]; ArgsThread arrArgsThread[cntThreads]; std::cout<<"POSIX threads. number of threads = "<<cntThreads<<std::endl; clock_t clockStart, clockStop; tms tmsStart, tmsStop; clockStart = times(&tmsStart); double step = 1./(double)num_steps; long long cntStepsPerThread= num_steps / cntThreads; //          for (unsigned int idThread=0; idThread<<cntThreads; idThread++) { arrArgsThread[idThread].left = idThread*cntStepsPerThread; arrArgsThread[idThread].right = (idThread+1)*cntStepsPerThread; arrArgsThread[idThread].step = step; if (pthread_create(&threads[idThread], NULL, worker, &arrArgsThread[idThread]) != 0) { return EXIT_FAILURE; } } //        join for (unsigned int idThread=0; idThread<cntThreads; idThread++) { if (pthread_join(threads[idThread], NULL) != 0) { return EXIT_FAILURE; } } //! clockStop = times(&tmsStop); std::cout << "The value of PI is " << pi << " Error is " << fabs(pi - PI25DT) << std::endl; std::cout << "The time to calculate PI was " ; double secs= (clockStop - clockStart)/static_cast<double>(sysconf(_SC_CLK_TCK)); std::cout << secs << " seconds\n" << std::endl; return 0; }
      
      







誰かが私の地獄のようなフォーマットが不均一に表示される場合に備えて、 http://dumpz.org/195404/にコピーします。



実際、この例では具体的でしたが(必ずしも幸運なわけではありません)、各スレッドの結果を個別の変数(配列要素ArgsThread arrArgsThread [ cntThreads ];

)そして、すべてのスレッドの完了を待った後、何が起こったのかをまとめます。



mutexのないコードは次のとおりです。



 #include <iostream> #include <iomanip> #include <cmath> #include <cstdlib> #include <pthread.h> #include <sys/times.h> #define cntThreads 4 struct ArgsThread { long long left,right; double step; double partialSum; }; static void *worker(void *ptrArgs) { ArgsThread * args = reinterpret_cast<ArgsThread *>(ptrArgs); double x; double sum=0.; double step=args->step; for (long long i=args->left; i<args->right; i++) { x = (i + .5)*step; sum = sum + 4.0/(1.+ x*x); } args->partialSum=sum*step; return NULL; } int main(int argc, char** argv) { const unsigned long num_steps=500000000; const double PI25DT = 3.141592653589793238462643; pthread_t threads[cntThreads]; ArgsThread arrArgsThread[cntThreads]; std::cout<<"POSIX threads. number of threads = "<<cntThreads<<std::endl; clock_t clockStart, clockStop; tms tmsStart, tmsStop; clockStart = times(&tmsStart); double step = 1./(double)num_steps; long long cntStepsPerThread= num_steps / cntThreads; for (unsigned int idThread=0; idThread<cntThreads; idThread++) { arrArgsThread[idThread].left = idThread*cntStepsPerThread; arrArgsThread[idThread].right = (idThread+1)*cntStepsPerThread; arrArgsThread[idThread].step = step; if (pthread_create(&threads[idThread], NULL, worker, &arrArgsThread[idThread]) != 0) { return EXIT_FAILURE; } } double pi=0.; for (unsigned int idThread=0; idThread<cntThreads; idThread++) { if (pthread_join(threads[idThread], NULL) != 0) { return EXIT_FAILURE; } pi +=arrArgsThread[idThread].partialSum; } clockStop = times(&tmsStop); std::cout << "The value of PI is " << pi << " Error is " << fabs(pi - PI25DT) << std::endl; std::cout << "The time to calculate PI was " ; double secs= (clockStop - clockStart)/static_cast<double>(sysconf(_SC_CLK_TCK)); std::cout << secs << " seconds\n" << std::endl; return 0; }
      
      





そして、 http://dumpz.org/195415/のコピー。



ご覧のとおり、コードはかなりかさばり、necroplatformであることがわかりました。 後者がboost ::スレッドを使用して部分的に解決される場合(ただし、誰もがboostをインストールするわけではありません)または新しいC ++ 11で、ストリームは一般的に言語の一部になります(非常にクールであることが判明しました)-しかし、ソフトウェアのほとんどは依然として古いC ++を使用しています。 しかし、面倒なコードの問題はまだ残っています。



Openmp



OpenMPは、言語(C / C ++ / Fortran)の拡張機能であり、オペレーティングシステムのスレッドAPIを使用した場合とほぼ同じことを行うことができますが、いわゆるプラグマの助けを借りれば、はるかに単純で簡潔になります。 プラグマは、そのままで、コンパイラーに「このコードを取得して並列に実行するように」指示し、残りはコピュレーターが行います。

最初の連続した例でforループを並列化するには、1行追加するだけです。

 #pragma omp parallel for private (x), reduction (+:sum) for (int i=0; i<numSteps; i++) { x = (i + .5)*step; sum = sum + 4.0/(1.+ x*x); }
      
      





このプラグマは、ループのターンを並列化し、変数xを各スレッドに対してプライベートにし、合計変数を使用して合計することで縮小(またはロシア語でどうですか?)を実行する必要があることを示しています。 つまり 最初に各ストリームのコピーを作成し、次に追加します。 ミューテックなしで前の例で行ったのと同じことについて。 OpenMPは、サービスのニーズに対応する小さなAPIも提供します。



 #include <iostream> #include <iomanip> #include <sys/times.h> #include <cmath> #include <omp.h> int main(int argc, char** argv) { const unsigned long numSteps=500000000; /* default # of rectangles */ double step; double PI25DT = 3.141592653589793238462643; double pi=0; double sum=0.0; double x; #pragma omp parallel { #pragma omp master { int cntThreads=omp_get_num_threads(); std::cout<<"OpenMP. number of threads = "<<cntThreads<<std::endl; } } clock_t clockStart, clockStop; tms tmsStart, tmsStop; step = 1./static_cast<double>(numSteps); clockStart = times(&tmsStart); #pragma omp parallel for private (x), reduction (+:sum) for (int i=0; i<numSteps; i++) { x = (i + .5)*step; sum = sum + 4.0/(1.+ x*x); } pi = sum*step; clockStop = times(&tmsStop); std::cout << "The value of PI is " << pi << " Error is " << fabs(pi - PI25DT) << std::endl; std::cout << "The time to calculate PI was " ; double secs= (clockStop - clockStart)/static_cast<double>(sysconf(_SC_CLK_TCK)); std::cout << secs << " seconds\n" << std::endl; return 0; }
      
      







http://dumpz.org/195550/のコピー。

g ++の場合は-fopenmpオプションを使用してコンパイルし、別のコンパイラの場合はユーザーマニュアルを参照してください。



質問やコメントを歓迎します。



All Articles