インテル® C++ コンパイラー XE 13.1 ユーザー・リファレンス・ガイド
自動ベクトライザーはインテル® コンパイラーのコンポーネントです。コンパイラーは、インテル® ストリーミング SIMD 拡張命令 (インテル® SSE、SSE2、SSE3 および SSE4 ベクトル化コンパイラー命令およびメディア・アクセラレーター命令)、インテル® ストリーミング SIMD 拡張命令 3 補足命令 (インテル® SSSE3) 、インテル® アドバンスト・ベクトル・エクステンション (インテル® AVX) 命令を自動的に使用します。 ベクトライザーは、並列に実行できるプログラム内の演算を検出し、データ型により、2、4、8、または 16 までの要素を並列で処理する 1 つの SIMD 命令に変換します。
ベクトル化とは一体何でしょうか? スカラー実装 (1 つの命令がオペランドの 1 ぺアに対する演算を行う) からベクトル処理 (1 つの命令がベクトル "一連の隣接した値" を参照できる) にアルゴリズムを変換するプロセスをベクトル化と呼びます。SIMD 命令は複数のデータ要素を 1 つの命令で演算し、128 ビット・レジスターを使用します。
自動ベクトル化は、インテル® コンパイラーがループをアンロールするためにパックド SIMD 命令を生成する際に行われます。パックド命令は一度に複数のデータ要素の演算を行うため、ループをより効率良く実行できます。このプロセスは、開発者が特別な処理をすることなく、コンパイラーが適したループを自動的に識別し最適化するため、自動ベクトル化と呼ばれることがあります。場合によっては、コードに特定のキーワードや宣言子を追加して、自動ベクトル化が行われるようにできす。
自動ベクトル化は、IA-32 アーキテクチャーとインテル® 64 アーキテクチャーでサポートされています。
-vec (Linux* および OS X*) または /Qvec (Windows*) オプションを使用すると、インテル製マイクロプロセッサーおよび互換マイクロプロセッサーにおいて、デフォルトの最適化レベルでベクトル化が有効になります。 ベクトル化により呼び出されるライブラリー・ルーチンは、互換マイクロプロセッサーよりもインテル製マイクロプロセッサーにおいてより優れたパフォーマンスが得られる可能性があります。また、ベクトル化は、/arch や /Qx (Windows*) または -m や -x (Linux* および 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* および OS X*) オプションを使用してベクトル化を無効にしたり、/O2 または -O2 オプションで有効にしてみてください。
ループがベクトル化されたがどうかを確認するには、/Qvec--report:1 /Qopt-report-phase hpo (Windows*) または –vec-report1 –opt-report-phase hpo (Linux* および OS X*) オプションを使用して、ベクトル化レポートを生成します。 次のように、ベクトル化された各ループに対して 1 行のメッセージが生成されます。
> icl /Qvec-report1 MultArray.c
MultArray.c(92): (列 5) リマーク: ループがベクトル化されました。
ソースの行番号 (上記の例では 92) はループの始めか終わりを指します。
適用されたループ変換と最適化の種類を確認するには、/Qopt-report-phase hpo (Windows*) または -opt-report-phase hpo (Linux* および OS X*) オプションをそれだけで指定するか、/Qopt-report (Windows*) または -opt-report (Linux* および OS X*) オプションと一緒に指定します。
パフォーマンスの向上はどの程度のものでしょうか? パフォーマンスの向上を評価するには、以下の例 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 つ目の反復で使用される前に最初の反復で上書きされ可能性があるため、安全ではありません。
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 は各反復で読み取りと書き込みの両方が行われますが、コンパイラーはこのようなリダクション・スタイルを認識し、安全にベクトル化することができます。2.3 節の例題 1 で示されるループは、スカラーの代わりにループ不変配列要素を持つリダクションの別の例です。
このようなループ反復間の依存性は、ループ伝搬の依存としても知られています。
上記の例では依存性が証明されています。しかし、コンパイラーは依存性の可能性があると、安全にループをベクトル化できません。次の例について考えてみます。
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 によりアクセスされるメモリー領域が (すべてか一部でも) オーバーラップする可能性があることを想定します。これにより、データ依存の可能性が発生し、このループのベクトル化が妨げられます。 この時点でコンパイラーはループをシリアルのままにすることを決定するか、以下のようなランタイムテストを生成し条件を満たすループをベクトル化します。
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* および OS 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* および OS X*) コンパイラー・オプションの使用を検討してください。 ただし、コンパイラー・オプションはソース全体に適用されるので、ほかのコード領域に影響を及ぼさないことを確認しなければなりません。