OpenMP* 関連のヒント
この記事は、インテル® ソフトウェア・サイトに掲載されている「OpenMP Related Tips」の日本語参考訳です。
OpenMP* ループの一重化
各スレッドで処理される作業の粒度を減らすことで利用可能なスレッド全体で分割される反復の総数を増やすには、OpenMP* collapse 節を使用します。 (ループを開いて一重化を行った後に) 各スレッドで処理される作業の量が多くなれば、OpenMP* アプリケーションの並列スケーラビリティーが向上します。
入れ子のループの内部で (可能な場合は) 一重化されたループのインデックスを使用しないようにすることで、パフォーマンスを向上させることができます (コンパイラーは divide/mod 操作により一重化されたループのインデックスからインデックスを再作成しなければならず、コンパイラー最適化の一部として不要コードを排除しないことで非常に複雑になるためです)。
collapse 節の使用例:
#pragma omp parallel for private(j) for (i = 0; i < imax; i++) { for (j = 0; j < jmax; j++) a[ j + jmax*i] = 1.; }
パフォーマンスが向上するようにcollapse 節を追加して修正した例:
#pragma omp parallel for private(j) collapse(2) for (i = 0; i < imax; i++) { for (j = 0; j < jmax; j++) a[ j + jmax * i] = 1.; }
OpenMP* 節の使用時によくあるミス
データ共有に関する適切な OpenMP* 節を追加していることを確認してください (キーワード private、reduction、その他を使用)。適切な節が追加されていない場合、プログラムでデータ競合が発生しますが、データ競合の非決定論影響により、設定によっては「正しく」動作しているように見えることがあります (ただし、正しく修正されたバージョンと比較するとパフォーマンス特性は大きく異なります)。
以下に例を示します:
データ競合が含まれる正しくないコード:
#pragma omp parallel for for(i=0; i < threads; i++) { offset=i*array_size; for(j=0; j < (iterations*(vec_ratio)); j++) { for(k=0; k < array_size; k++) { sum1 += a[k+offset] * s1; sum2 += a[k+offset] * s2; } } }
データ競合が排除された修正バージョン:
#pragma omp parallel for num_threads(threads) reduction(+:sum1,sum2) for(i=0; i < threads; i++) { float sum1_local=0.0f; float sum2_local=0.0f; int offset=i*array_size; for(int j=0; j < (iterations*(vec_ratio)); j++) { for(int k=0; k < array_size; k++) { sum1_local += a[k+offset] * s1; sum2_local += a[k+offset] * s2; } } sum1 += sum1_local; sum2 += sum2_local; }
barrier 同期のオーバーヘッドの削減
多数のスレッドで barrier 同期を行うと、OpenMP* 構文によってはオーバーヘッドが著しく増加することがあります。この場合は、“nowait” 節を使用することで待機によるオーバーヘッドを軽減できます。“nowait” 節を並列領域内の omp for ループの static スケジュールに使用した以下の例を参照してください。この節は、処理の個々の部分が完了する際にスレッドが同期しないことを意味します。
同じスレッドが各ループの同じ反復番号を実行する (スレッドが異なる image_id 値の内部ループの同じ反復を常に実行する) ため、static スケジュールで “nowait” 節を使用しても問題ありません。(スケジュール・タイプを dynamic に変更すると “nowait” 節は不正になります)。
void *task(void* tid_, float **Raw, float *Vol) { long int tid = (long int) tid_; if(tid==0) printf("bptask_vgather\n"); int _kk_; int il,jl,kl,i,j,k,kbase; __int64 cycles=0; #pragma omp parallel for(int image_id =0; image_id < global_number_of_projection_images; image_id++) { float *local_Raw = Raw[image_id]; int chunksize = NBLOCKS/nthreads; // nthreads==1, tid==0 int beg_ = tid*chunksize; int end_ = (tid+1)*chunksize; #pragma omp for schedule(static) nowait for(int b=beg_; b < end_; b++) { { int K = ((b%NBLOCKSDIM_K) << BLOCKDIMBITS_K); int t=b/NBLOCKSDIM_K; int J = ((t%NBLOCKSDIM_J) << BLOCKDIMBITS_J); t=t/NBLOCKSDIM_J; int I = ((t%NBLOCKSDIM_I) << BLOCKDIMBITS_I); float *localVol=Vol+b*BLOCKDIM_I*BLOCKDIM_J*BLOCKDIM_K; for(int i=I; i < I+BLOCKDIM_I; i++) { for(int j=J; j < J+BLOCKDIM_J; j++) { float tmp0 = c00*i+c01*j+c03; float tmp1 = c10*i+c11*j+c13; float tmp3 = c30*i+c31*j+c33; #pragma simd for(int k=K; k < K+BLOCKDIM_K; k++) { float w=1/(tmp3+c32*k); float lreal=w*(tmp1+c12*k); // y float mreal=w*(tmp0+c02*k); // x int l=(int)(lreal); float wl=lreal-l; int m=(int)(mreal); float wm=mreal-m; (*localVol) += w*w*( (1-wl) * ((1-wm)*local_Raw[l*WinY +m ] + wm*local_Raw[l*WinY +m+1]) + wl * ((1-wm)*local_Raw[l*WinY+WinY+m ] + wm*local_Raw[l*WinY+WinY+m+1]) ); localVol++; } } } } } } return NULL; }
次のステップ
この記事は、「Programming and Compiling for Intel® Many Integrated Core Architecture」の一部「OpenMP Related Tips」の翻訳です。インテル® Xeon Phi™ コプロセッサー上にアプリケーションを移植し、チューニングを行うには、各リンクのトピックを参照してください。アプリケーションのパフォーマンスを最大限に引き出すために必要なステップを紹介しています。
コンパイラーの最適化に関する詳細は、最適化に関する注意事項を参照してください。