インテル® C++ コンパイラー 19.0 デベロッパー・ガイドおよびリファレンス
自動ベクトル化は、IA-32 アーキテクチャーとインテル® 64 アーキテクチャーでサポートされています。ここでは、自動ベクトルの設定に役立つ情報を提供します。
ベクトル化によるスピードアップはどのようにもたらされるでしょうか? 次に示すコード例について考えてみます。a、b、c は整数配列です。
コード例 |
---|
|
ベクトル化が有効ではない場合 (つまり、O1 または [Q]vec- オプションを使用してコンパイルする場合)、コンパイラーはそれぞれの反復で各 SIMD レジスターに追加で 3 つの整数を保持できる空間があったとしても、それらを活用せずに (未使用のまま) コードを生成します。ベクトル化が有効な場合 (O2 以上のオプションを使用してコンパイルする場合)、コンパイラーは SIMD レジスターの未使用の空間を活用して、1 つの命令で 4 つの加算を実行します。デフォルトの最適化オプション (-O2) 以上でコンパイルすると、コンパイラーは常にベクトル化の可能性を探します。
このオプションを使用すると、インテル製マイクロプロセッサーおよび互換マイクロプロセッサーの両方で、デフォルトの最適化レベルのベクトル化が有効になります。ベクトル化により呼び出されるライブラリー・ルーチンは、互換マイクロプロセッサーよりもインテル製マイクロプロセッサーにおいてより優れたパフォーマンスが得られる可能性があります。また、ベクトル化は、/arch (Windows*)、-m (Linux* および macOS*)、[Q]x などの特定のオプションによる影響を受けます。
ベクトル化されたコードとベクトル化されていないコードを比較するために、/Qvec- オプション (Windows*) または -no-vec オプション (Linux* および macOS*) を使用してベクトル化を無効にしたり、O2 オプションで有効にしてみてください。
ループがベクトル化されたがどうかを確認するには、Qopt-report:1 Qopt-report-phase:vec (Windows*) または qopt-report=1 qopt-report-phase=vec (Linux* および macOS*) オプションを使用して、最適化化レポートを生成します。これらのオプションは、*.optrpt ファイルに最適化メッセージを含む異なるレポートを生成します。Visual Studio* では、ソースファイルにレポートメッセージのアノテーションが付けられます。生成された .optrpt ファイルをテキストエディターで開いて確認することもできます。次のように、ベクトル化されたループごとにメッセージが出力されます。
ベクトル化レポートの例 |
---|
|
ソースの行番号 (上記の例では 92) はループの始めか終わりを指します。
適用されたループ変換と最適化の種類を確認するには、[Q]opt-report-phase オプションだけを指定するか、[Q]opt-report オプションと一緒に指定します。
Visual Studio* IDE を使用してループがベクトル化されたかどうかの情報を得るには、[プロジェクト] > [プロパティ] > [C/C++] > [Diagnostics (診断)] > [Optimization Diagnostic Level (最適化診断レベル)] で [Level 1 (/Qopt-report:1) (レベル 1 (/Qopt-report:1))] を、[Optimization Diagnostic Phase (最適化診断フェーズ)] で [Loop Nest Optimization (/Qopt-report-phase:loop) (ループの入れ子構造の最適化 (/Qopt-report-phase:loop))] を選択します。ベクトル化されなかった各ループに対する診断メッセージと、ベクトル化されなかった理由の説明を取得するには、/Qopt-report-phase:vec を選択します。
パフォーマンスの向上はどの程度のものでしょうか? パフォーマンスの向上を評価するには、以下の例 vec_samples を実行してみてください。
インテル® コンパイラーのコマンドウィンドウを開きます。
Windows*:[スタート] メニューのインテル製品項目にある [Compiler and Performance Libraries (コンパイラーおよびパフォーマンス・ライブラリー)] > [Command Prompt with Intel Compiler (インテル® コンパイラー用コマンドプロンプト)] からアイコンを選択します。
Linux* および macOS*: <installdir>/bin ディレクトリーにある compilervars.sh または compilervars.csh などの環境スクリプトを、アーキテクチャーに応じた属性を使用して source コマンドで実行します。
<install-dir>\Samples\<locale>\C++\ ディレクトリーに移動します。Windows* 上で、vec_samples.zip サンプル・プロジェクトを書き込み可能なディレクトリーに展開します。この小さなプログラム example1 は、次のループを使用してベクトルに行列を掛けます。
ベクトル行列乗算の例 |
---|
|
最初は自動ベクトル化を無効にして、プログラムをビルドし実行します。デフォルトの O2 最適化は、ベクトル化を有効にするため、別のオプションにより無効にする必要があります。プログラムの実行時間に注目してください。
自動ベクトル化なしのアプリケーションのビルドと実行の例 |
---|
|
|
次に自動ベクトル化を使用して、プログラムをビルドし実行します。プログラムの実行時間に注目してください。
自動ベクトル化ありのアプリケーションのビルドと実行の例 |
---|
|
|
2 つの実行時間を比較すると、ベクトル化されたバージョンのほうが速いことが分かるでしょう。ベクトル化されていないバージョンの実行時間は、O1 オプションでコンパイルした場合より多少早いだけです。
次に示す状況が必ずしもベクトル化を妨げるわけではありませんが、コンパイラーがベクトル化のメリットがないと判断する原因になります。
連続していないメモリーアクセス: 4 つの連続する int 型または float 型、あるいは 2 つの連続する double 型は、1 つの SSE 命令でメモリーから直接ロードされます。しかし、4 つの整数が隣接していなければ、複数の命令を使って別々にロードしなければならず、非効率的です。連続していないメモリーアクセスの最も一般的な例は、次に示すような非ユニットストライドのループや間接アドレス指定のループです。連続していないメモリーアクセスのオーバーヘッドと比較して計算量が多くない限り、コンパイラーはそのようなループをほとんどベクトル化しません。
連続していないメモリーアクセスの例 |
---|
|
通常、ベクトル化レポートには「ベクトル化は可能ですが非効率です」と出力されます。間接アドレス指定に対しては、「ベクトル依存関係が存在しています」というメッセージが出力されることもあります。
データ依存: 各 SIMD 命令は複数のデータ要素を一度に演算するため、ベクトル化ではループ内の演算順序が変わります。この順序の変更が演算結果に影響しない場合にのみ、ベクトル化が可能です。
最も単純なケースは、ループで書き込まれる (格納される) データ要素がそのループのほかの反復では使用されない場合です。この場合は、オリジナルのループのすべての反復が互いに独立しており、結果を変更することなくどの順序でも実行できます。このようなループは、ベクトル化を含むあらゆる並列手法で安全に実行することできます。これまでに示した例はすべてこのカテゴリーに分類されます。
変数が 1 つの反復で書き込まれ、後に続く反復で読み取られる場合は、"リードアフターライト" の依存性 (フロー依存性とも呼ばれる) があります。次に例を示します。
フロー依存性の例 |
---|
|
j の値はすべての A[j] に含まれます。この場合、安全にベクトル化できません。最初の 2 つの反復が SIMD 命令で同時に実行されると、最初の反復による演算前に、A[1] の値が 2 つ目の反復で使用されます。
変数が 1 つの反復で読み取られ、後に続く反復で書き込まれる場合は、"ライトアフターリード" の依存性 (アンチ依存とも呼ばれる) があります。次に例を示します。
ライトアフターリードの依存性の例 |
---|
|
このライトアフターリードの依存性の場合、書き込みの反復が、読み取りの反復前に実行される可能性があるため、一般的な並列実行では安全ではありません。しかし、この例では j の値が大きい反復が小さい反復よりも前に完了することはありません。そのため、この場合は安全にベクトル化できます (つまり、非ベクトルコードと同じ結果がもたらされます)。ただし、次の例では、ベクトル化により、A の要素が 2 つ目の反復で使用される前に最初の反復で上書きされ可能性があるため、安全ではありません。
安全ではないベクトル化の例 |
---|
|
リードアフターリードは、実際には依存ではなく、ベクトル化や並列実行を妨げるわけではありません。変数が書き込まれなければ、いくら読み取られても関係ありません。
同じ変数が複数の反復で書き込まれるライトアフターライト、または「出力」依存性は、一般にベクトル化を含む並列実行で安全ではありません。
上記の依存性タイプすべてを含む重要な例外があります:
依存性の例外 |
---|
|
sum は各反復で読み取りと書き込みの両方が行われますが、コンパイラーはこのようなリダクション・スタイルを認識し、安全にベクトル化することができます。最初の例のループは、スカラーの代わりにループ不変配列要素を持つリダクションの別の例です。
このようなループ反復間の依存性は、ループ伝搬の依存としても知られています。
上記の例では依存性が証明されています。コンパイラーは依存性の可能性があると、安全にループをベクトル化できません。次の例について考えてみます。
依存性の可能性がある例 |
---|
|
上記の例では、コンパイラーは、反復 I で c[i] が別の反復の a[i] または b[i] と同じメモリー位置を参照するかどうかを判別しなければなりません。そのようなメモリー位置は "エイリアス" と呼ばれます。例えば、a[i] が c[i-1] と同じメモリー位置を指した場合、前述したようにリードアフターライトの依存性があります。この可能性をコンパイラーが排除できない場合、開発者がコンパイラーにヒントを提供しない限り、ループはベクトル化されません。
場合によっては、インテル® C++ コンパイラーがループのベクトル化を判断するための情報が不足していることがあります。コンパイラーに追加情報を提供する方法はいくつかあります。
#pragma ivdep: 可能性のあるデータ依存を無視しても安全であることをコンパイラーに知らせます (コンパイラーは自身が検出した依存性は無視しません)。明らかな依存性がある場合にこのプラグマを使用すると、不正な結果を引き起こします。
安全にベクトル化できることが分かっていても、コンパイラーのスタティック依存性の解析では判断できない場合があります。次のループについて考えてみます。
ループの例 |
---|
|
追加ヒントがなければ、コンパイラーは、ポインター変数 cp_a と cp_b によりアクセスされるメモリー領域が (すべてか一部でも) オーバーラップする可能性があることを想定します。これにより、データ依存の可能性が発生し、このループのベクトル化が妨げられます。この時点でコンパイラーはループをシリアルのままにすることを決定するか、以下のようなランタイムテストを生成し条件を満たすループをベクトル化します。
条件を満たすループの例 |
---|
|
ランタイムのデータ依存性テストは、コードサイズとテスト・オーバーヘッドは増えますが、一般に C または C++ コードで並列処理を活用する効率的な方法を提供します。ただし、関数自身にヒントが指定される場合、次のようにコンパイラーのベクトル化を支援できます。
#pragma ivdep によりデータ依存性を無視する例 |
---|
|
restrict キーワードを使用することもできます。
restrict キーワードを上記の cp_a と cp_b の宣言で使用し、各ポインター変数が特定のメモリー領域への排他アクセスを提供することをコンパイラーに知らせることができます。引数リストの restrict 指示子は、ポインターが指すメモリーへのエイリアスがほかにないことをコンパイラーに知らせます。つまり、使用されるポインターは、そのポインターが有効なスコープ内でそのメモリーへアクセスする唯一の方法であることを示します。コードが restrictキーワードなしでベクトル化されたとしても、コンパイラーは restrict キーワードが使用されると、ランタイム時にエイリアシングをチェックします。インテル® C++ コンパイラーでは、[Q]restrict コンパイラー・オプションなど、追加のオプションを使用しなければならないことがあります。
Restrict キーワードの例 |
---|
|
この手法は、ソース中の多くのループで排他アクセスの可能性があるポインター変数を利用している場合に便利です。ベクトル化が可能なループにそれぞれヒントを加える必要がないためです。ただし、ループ固有の #pragma ivdep ヒントと、ポインター変数固有の restrict ヒントの使用には注意してください。誤った使用は、オリジナルのプログラムのセマンティクスを変更することがあります。
次の例では、ポインター a、b、c の間でエイリアス問題の可能性があるため、ベクトル化されません。
サポートされていない可能性のあるループ構造の例 |
---|
|
restrict キーワードが追加されると、コンパイラーはそのメモリーにアクセスするほかのポインターがないことを想定し、コードをベクトル化します。
Restrict キーワードによるポインターの使用例 |
---|
|
restrict を使用する欠点は、すべてのコンパイラーがこのキーワードをサポートしているわけではないことです。そのため、ソースの移植性が低下します。ソースコードの移植性を重視する場合は、代わりに [Q]ansi-alias コンパイラー・オプションの使用を検討してください。ただし、コンパイラー・オプションはソース全体に適用されるので、ほかのコード領域に影響を及ぼさないことを確認しなければなりません。
/Qansi-alias (-fargument-alias) オプションは、ISO C 規格のエイリアス規則に厳密に準拠することを有効にします。これらのオプションは責任を持って使用してください。メモリーがエイリアスされる場合にこれらのオプションを使用すると、正しくない結果を招きます。
[Q]ansi-alias オプションが指定された場合、ansi-alias チェッカーはデフォルトで有効になります。チェッカーを無効にするには、-no-ansi-alias-check (Linux* および macOS*) または /Qansi-alias-check (Windows*) を指定します。
[Q]ansi-alias-check オプションを使用して、ansi-alias チェッカーを有効にします。ansi-alias チェッカーは、ANSI エイリアシング規則に違反している可能性がないかソースコードを確認し、違反の可能性があると判断された命令文のコードに関連する安全でない最適化を無効にします。