自動ベクトライザーはインテル® コンパイラーのコンポーネントです。コンパイラーは、インテル® ストリーミング SIMD 拡張命令 (インテル® SSE、SSE2、SSE3 および SSE4 ベクトル化コンパイラー命令およびメディア・アクセラレーター命令)、ストリーミング SIMD 拡張命令 3 補足命令 (SSSE3)、インテル® Advanced Vector Extension (インテル® AVX) 命令を自動的に使用します。ベクトライザーは、並列に実行できるプログラム内の演算を検出し、データ型により、2、4、8、または 16 までの要素を並列で処理する 1 つの SIMD 命令に変換します。
ベクトル化とは一体何でしょうか? スカラー実装 (一度にオペランドの 1 ぺアに対する演算を行う) からベクトル処理 (1 つの命令がベクトル (一連の隣接した値) を参照できる) にアルゴリズムを変換するプロセスをベクトル化と呼びます。SIMD 命令は複数のデータ要素を 1 つの命令で演算し、128 ビット浮動小数点レジスターを使用します。
自動ベクトル化は、インテル® コンパイラーがループをアンロールするためにパックド SIMD 命令を生成する際に行われます。パックド命令は一度に複数のデータ要素で演算するため、ループがより効率良く実行されます。このプロセスは、開発者が特別な処理をすることなく、コンパイラーが適したループを独自に識別し最適化するため、自動ベクトル化と呼ばれています。場合によっては、コードに特定のキーワードや宣言子を追加して、自動ベクトル化が行われるようにすることができます。
自動ベクトル化は、IA-32 アーキテクチャーとインテル® 64 アーキテクチャーでサポートされています。
-vec (Linux* および Mac OS* X) または /Qvec (Windows*) オプションを使用すると、インテル製マイクロプロセッサーおよび互換マイクロプロセッサーにおいて、デフォルトの最適化レベルでベクトル化が有効になります。ベクトル化により呼び出されるライブラリー・ルーチンは、互換マイクロプロセッサーよりもインテル製マイクロプロセッサーにおいてより優れたパフォーマンスが得られる可能性があります。また、ベクトル化は、/arch や /Qx (Windows*) または -m や -x (Linux* および Mac OS* X) などの特定のオプションによる影響を受けます。
ベクトル化によるスピードアップはどのようにもたらされるでしょうか? 次に示すコード例について考えてみます。a、b、c は整数配列です。
for (i=0;i<=MAX;i++)
c[i]=a[i]+b[i];
ベクトル化が有効ではない場合 (つまり、/O1 または /Qvec- オプションを使用してコンパイルする場合)、コンパイラーはそれぞれの反復で各 SIMD レジスターに追加で 3 つの整数を保持できる空間があったとしても、それらを活用せずに (未使用のまま) コードを生成します。ベクトル化が有効な場合 (/O2 以上のオプションを使用してコンパイルする場合)、コンパイラーは SIMD レジスターの未使用の空間を活用して、1 つの命令で 4 つの加算を実行します。デフォルトの最適化オプション (-O2) またはそれ以上でコンパイルすると、コンパイラーは常にベクトル化の機会を探します。
ベクトル化されたコードとベクトル化されていないコードを比較するために、/Qvec- (Windows*) または -no-vec (Linux* および Mac OS* X) オプションを使用してベクトル化を無効にしたり、/O2 または -O2 オプションで有効にしてみてください。
ループがベクトル化されたがどうかを確認するには、/Qopt-report:1 /Qopt-report-phase hpo (Windows*) または -opt-report1 -opt-report-phase hpo (Linux* および Mac OS* X) オプションを使用して、ベクトル化レポートを生成します。次のように、ベクトル化された各ループに対して 1 行のメッセージが生成されます。
> icl /Qvec-report1 MultArray.c
MultArray.c(92): (列 5) リマーク: ループがベクトル化されました。
ソースの行番号 (上記の例では 92) はループの始めか終わりを指します。
パフォーマンスの向上はどの程度のものでしょうか? パフォーマンスの向上を評価するには、以下の例 example1 を実行してみてください。
for (j = 0;j < size2; j++) {
b[i] += a[i][j] * x[j];
}
icc -O2 -no-vec MultArray.c -o NoVectMult
./NoVectMult
icc -O2 -vec-report1 MultArray.c -o VectMult
./VectMult
2 つの実行時間を比較すると、ベクトル化されたバージョンのほうが速いことがわかるでしょう。ベクトル化されていないバージョンの実行時間は、/O1 オプションまたは -O1 オプションでコンパイルした場合より多少早いだけです。
次に示す状況が必ずベクトル化を妨げるわけではありませんが、頻繁にベクトル化を阻んだり、コンパイラーがベクトル化の利点がないと判断する原因になります。
// ストライド 2 で配列にアクセス
for (int i=0; i<SIZE; i+=2) b[i] += a[i] * x[i];
// ストライド SIZE で内側のループにアクセス
for (int j=0; j<SIZE; j++) {
for (int i=0; i<SIZE; i++) b[i] += a[i][j] * x[j];
}
// 配列を使用した x の間接アドレス指定
for (int i=0; i<SIZE; i+=2) b[i] += a[i] * x[index[i]];
通常、ベクトル化レポートには「ベクトル化は可能ですが非効率です」と出力されます。間接アドレス指定に対しては、「ベクトル依存関係が存在しています」というメッセージが出力されることもあります。
A[0]=0;
for (j=1; j<MAX; j++) A[j]=A[j-1]+1;
// これは、次と同じ
A[1]=A[0]+1; A[2]=A[1]+1; A[3]=A[2]+1; A[4]=A[3]+1;
j の値はすべての A[j] に含まれます。この場合、安全にベクトル化できません。最初の 2 つの反復が SIMD 命令で同時に実行されると、最初の反復による演算前に、A[1] の値が 2 つ目の反復で使用されます。
for (j=1; j<MAX; j++) A[j-1]=A[j]+1;
// これは、次と同じ
A[0]=A[1]+1; A[1]=A[2]+1; A[2]=A[3]+1; A[3]=A[4]+1;
この場合、書き込みの反復が、読み取りの反復前に実行される可能性があるため、一般的な並列実行では安全ではありません。しかし、この例では j の値が大きな反復が小さな反復よりも前に完了することはありません。そのため、この場合は安全にベクトル化できます (例えば、非ベクトル化コードと同じ結果がもたらされるなど)。ただし、次の例では、ベクトル化により、A のいくつかの要素が 2 つ目の SIMD 命令で使用される前に 1 つ目の SIMD 命令で上書きされ可能性があるため、安全ではありません。
for (j=1; j<MAX; j++) {
A[j-1]=A[j]+1;
B[j]=A[j]*2;
}
// これは、次と同じ
A[0]=A[1]+1; A[1]=A[2]+1; A[2]=A[3]+1; A[3]=A[4]+1;
sum=0;
for (j=1; j<MAX; j++) sum = sum + A[j]*B[j]
sum は各反復で読み取りと書き込みの両方が行われますが、コンパイラーはこのようなリダクション・スタイルを認識し、安全にベクトル化することができます。example1 のループは、スカラーの代わりにループ不変配列要素を持つリダクションの別の例です。
ループ反復間のこのような依存性は、ループ運搬の依存としても知られています。
上記の例では依存性が証明されています。しかし、コンパイラーは依存性の可能性があると、安全にループをベクトル化できません。次の例について考えてみます。
for (i = 0; i < size; i++) {
c[i] = a[i] * b[i];
}上記の例では、コンパイラーは、反復 i で c[i] が別の反復の a[i] または b[i] と同じメモリー位置を参照するかどうかを判別しなければなりません (そのようなメモリー位置は “エイリアス” と呼ばれます)。例えば、a[i] が c[i-1] と同じメモリー位置を指した場合、前述したようにリードアフターライトの依存性があります。この可能性をコンパイラーが排除できない場合、開発者がコンパイラーにヒントを提供しない限り、ループはベクトル化されません。
場合によっては、コンパイラーがループのベクトル化を判断するための情報が不足していることがあります。コンパイラーに追加情報を提供する方法はいくつかあります。
void copy(char *cp_a, char *cp_b, int n) {
for (int i = 0; i < n; i++) {
cp_a[i] = cp_b[i];
}
}
追加情報がなければ、ベクトル化コンパイラーは、ポインター変数 cp_a と cp_b によりアクセスされるメモリー領域が (一部) オーバーラップする可能性があると仮定しなければなりません。これにより、データ依存の可能性が発生し、このループの SIMD 命令への単純な変換が妨げられます。この時点でコンパイラーはループをシリアルのままにすることを決定するか、オーバーラップのランタイムテストを生成し True 分岐のループを SIMD 命令に変換します。
if (cp_a + n < cp_b || cp_b + n < cp_a)
/* ベクトル化ループ */
for (int i = 0; i < n; i++) cp_a[i] = cp_b[i];
else
/* シリアルループ */
for (int i = 0; i < n; i++) cp_a[i] = cp_b[i];
ランタイムのデータ依存性テストは、コードサイズとテスト・オーバーヘッドは増えますが、一般に C または C++ コードで暗黙的な並列処理を活用する効率的な方法を提供します。ただし、関数のコピーが特定の方法でのみ使用される場合、次のようにコンパイラーのベクトル化を支援できます。
#pragma ivdep
void copy(char *cp_a, char *cp_b, int n) {
for (int i = 0; i < n; i++) {
cp_a[i] = cp_b[i];
}
}
restrict キーワードを使用することもできます。
restrict キーワードを上記の cp_a と cp_b の宣言で使用し、各ポインター変数が特定のメモリー領域への排他アクセスを提供することをコンパイラーに知らせることができます。引数リストの restrict 指示子は、ポインターが指すメモリーへのエイリアスがほかにないことをコンパイラーに知らせます。つまり、使用されるポインターは、そのポインターが有効なスコープ内で問題のメモリーへアクセスする唯一の方法を提供します。コードが restrict キーワードなしでベクトル化されたとしても、コンパイラーは restrict キーワードが使用されると、ランタイム時にエイリアシングをチェックします。インテル® C/C++ コンパイラーでは、/Qrestrict (Windows*) または -restrict (Linux* および MacOS* X) コンパイラー・オプションなど、追加のオプションを使用しなければならないことがあります。
void copy(char * __restrict cp_a, char * __restrict cp_b,
int n) {
for (int i = 0; i < n; i++) cp_a[i] = cp_b[i];
}
この手法は、排他アクセスのプロパティーが、多くのループを含むコードの大部分で使用されるポインター変数を保持する場合に便利です。ベクトル化が可能なループにそれぞれアノテーションを付ける必要がないためです。ただし、ループ固有の #pragma ivdep ヒントと、ポインター変数固有の restrict ヒントは注意して使用してください。誤った使用は、オリジナルのプログラムのセマンティクスを変更することがあります。
次のループの例では、ポインター a、b および c の間でエイリアス問題の可能性があるため、ベクトル化されません。
// サポートされていない可能性があるループ構造
void add(float *a, float *b, float *c) {
for (int i=0; i<SIZE; i++) {
c[i] += a[i] + b[i];
}
}
restrict キーワードがパラメーターに追加されると、コンパイラーは問題のメモリーにアクセスする他のポインターがないことを想定し、コードを適切にベクトル化します。
// restrict を指定してポインターが安全であることをコンパイラーに知らせる
void add(float * __restrict a, float * __restrict b, float * __restrict c) {
for (int i=0; i<SIZE; i++) {
c[i] += a[i] + b[i];
}
}
restrict を使用する短所は、すべてのコンパイラーがこのキーワードをサポートしているわけではない点です。そのため、コードの移植性が低下します。ソースコードの移植性を重視する場合は、代わりに /Qansi-alias (Windows*) または -ansi-alias (Linux* および MacOS* X) コンパイラー・オプションの使用を検討してください。ただし、コンパイラー・オプションはグローバルに適用されるので、他のコード・フラグメントに悪影響を及ぼさないことを確認する必要があります。
© 1996-2011 Intel Corporation. 無断での引用、転載を禁じます。