この記事は、インテル® デベロッパー・ゾーンに掲載されている「Outer Loop Vectorization」(http://software.intel.com/en-us/articles/outer-loop-vectorization) の日本語参考訳です。
はじめに
外部ループのベクトル化は、パフォーマンスを向上するテクニックの 1 つです。デフォルトでは、コンパイラーは入れ子のループ構造の最内ループをベクトル化しようと試みます。しかし、最内ループの反復回数が少ない場合、内部ループのベクトル化は効果的ではありません。しかし、外部ループに多くのワークが含まれている場合、SIMD 対応関数 (旧称: 要素関数)、ストリップマイニング (ループ・ブロッキング)、SIMD プラグマ/ディレクティブを組み合わせて、効果的に外部ループをベクトル化できます。
外部ループのベクトル化の例を次に示します。
入れ子のループの例:
以下のスパース-行列-ベクトルパターンについて考えてみます。
for(LocalOrdinalType row=ib*BLOCKSIZE; row<top; ++row) { local_y[row]=0.0; for(LocalOrdinalType i=Arowoffsets[row]; i<Arowoffsets[row+1]; ++i) { local_y[row] += Acoefs[i]*local_x[Acols[i]]; } }
内部ループのベクトル化と外部ループのベクトル化:
内部の i ループをベクトル化できます。デフォルトのベクトル化では、配列 "Acoefs" と "Acols" のユニットストライド方式のロードと、配列 "local_x" の要素のギャザーが行われます。"local_y" ストレージアクセスはループの外に移動され、コンパイラーによりベクトル化されたループにリダクション temp が追加されます。
あるいは、(SIMD 対応関数を利用して) 外部ループをベクトル化することができます。この場合、コンパイラーは内部ループ本体に対して、"local_y"、"Acoefs"、"Acols" ロードがギャザーになり、"local_x" が引き続きギャザーになるようにユニットストライド方式のロード/ストア命令を生成します。
このアプローチが効果的な理由:
内部ループのトリップカウントの平均が小さく (ベクトル化するには小さすぎる)、外部ループのトリップカウントが大きい場合、ベクトル化の効率が向上します。その他の考慮事項として、外部ループのベクトル化により、ベクトル化された外部ループで (ループ分割などの) 処理コストが増加しないかがあります。内部ループのベクトル化と比較して、パフォーマンスの向上率と余分なギャザー/スキャッターのコストを検討する必要があります。また、外部ループのベクトル化ではアライメントの指定が困難です。
外部ループをベクトル化する方法:
- 外部ループに simd プラグマ (と適切な節) を追加します。または、対応する OpenMP* の #pragma omp simd と適切な節を使用します。
- 外部ループの本体を SIMD 対応関数として抽出し、simd プラグマを用いて外部ループを直接ベクトル化します。
- ループ・ブロッキング (2 次元以上のストリップマイニング) の例は、『インテル® C++ コンパイラー・デベロッパー・ガイドおよびリファレンス』 (英語) を参照してください。
前の例では、1 番目の方法を使用して、外部ループに #pragma simd を追加するだけで、外部ループをベクトル化できます。
または、次に示すように、2 番目の方法を使用してベクトル化することもできます。
#pragma simd for(LocalOrdinalType row=ib*BLOCKSIZE; row<top; ++row) { local_y[row]=0.0; Inner_loop_elem_function(local_y, row, Acoefs, local_x, Acols, Arowoffsets); } __declspec(vector(uniform(Arowoffsets, Acoefs, local_x, Acols, local_y), linear(row)))) Inner_loop_elem_function(float *local_y, int row, float *Acoefs, float *local_x, int *Acols, int *Arowoffsets) { for(LocalOrdinalType i=Arowoffsets[row]; i<Arowoffsets[row+1]; ++i) { local_y[row] += Acoefs[i]*local_x[Acols[i]]; } }
SIMD 対応関数を使った外部ループのベクトル化の別の例を次に示します。
#pragma simd // simd pragma for outer-loop at call-site of SIMD-enabled function for (int i = beg*16; i < end*16; ++i) { particleVelocity_block(px[i], py[i], pz[i], destvx + i, destvy + i, destvz + i, vel_block_start, vel_block_end); }
SIMD 対応関数の定義:
__declspec(vector(uniform(start,end), linear(velx,vely,velz))) static void particleVelocity_block(const float posx, const float posy, const float posz, float *velx, float *vely, float *velz, int start, int end) { __assume_aligned(velx,64); __assume_aligned(vely,64); __assume_aligned(velz,64); for (int j = start; j < end; ++j) { const float del_p_x = posx - px[j]; const float del_p_y = posy - py[j]; const float del_p_z = posz - pz[j]; const float dxn = del_p_x * del_p_x + del_p_y * del_p_y + del_p_z * del_p_z +pa[j]* pa[j]; const float dxctaui = del_p_y * tz[j] - ty[j] * del_p_z; const float dyctaui = del_p_z * tx[j] - tz[j] * del_p_x; const float dzctaui = del_p_x * ty[j] - tx[j] * del_p_y; const float dst = 1.0f/std::sqrt(dxn); const float dst3 = dst*dst*dst; *velx -= dxctaui * dst3; *vely -= dyctaui * dst3; *velz -= dzctaui * dst3; } }
SIMD 対応関数の uniform 節と linear 節の使用に注意してください。最後の 2 つの関数の引数 vel_block_start と vel_block_end はすべての i で同じであるため、これらの 2 つの引数は uniform としてマークされます。ポインター引数 velx、vely、velz は、単一の呼び出し位置に対応する linear としてマークされます。
これらのポインター変数は、SIMD 対応関数内部の適切な節を用いて (呼び出し位置でポインターのアライメント情報を利用することにより) アラインとしてもマークされます。
次の ISCA 2012 の論文では、外部ループのベクトル化の例が多数紹介されています: 「Can Traditional Programming Bridge the Ninja Performance Gap for Parallel Computing Applications?」 (英語) (2012 年 6 月)
まとめ
デフォルトでは、コンパイラーは入れ子のループ構造の最内ループをベクトル化しようと試みます。この最内ループの反復回数が少ない場合、最内ループのベクトル化は効果的ではありません。しかし、外部ループの反復回数が多い場合、外部ループのベクトル化が効果的です。外部ループをベクトル化するには、SIMD 対応関数と SIMD プラグマ/ディレクティブを組み合わせてベクトル化を外部レベルに移動します。
次のステップ
インテル® アドバンスト・ベクトル・エクステンション (インテル® AVX) 対応インテル® Xeon® プロセッサー上にアプリケーションを移植してチューニングを行うには、各リンクのトピックを参照してください。
「ベクトル化の基本」に戻る
コンパイラーの最適化に関する詳細は、最適化に関する注意事項を参照してください。