Visual* C++、GCC*、インテル® C++ コンパイラーなど、広く利用されているコンパイラーには様々な最適化オプションが用意されています。この連載では、C/C++ソースのコンパイル時にパフォーマンスに影響する幾つかの最適化オプションの使い方とその効果を説明します。
第3回 インテル® コンパイラーのベクトル化レポートを活用する
基本的にコンパイラーは皆さんの書いたソースをコンパイルする際に、どのソース行をベクトル化したか通知しません。アセンブリーソースを出力して調査すれば解りますが、それでは本末転倒です。今回は、インテル® C++ コンパイラーが提供するベクトル化のレポートとアシスト機能について紹介します。この記事の説明のため利用したコンパイラーのバージョンは、「Windows 版 インテル® 64 対応 インテル® C++ コンパイラー XE 12.0.3.176 (update3 日本語版)」です。
ベクトル化レポートの生成
インテル® C++ コンパイラーのベクトル化レポート機能は、コンパイラーがソースコードを解析しバイナリーを生成する際に、次の情報を表示する機能です。
① ベクトル化できたコード領域
② ベクトル化できなかったコード領域 および その原因
この機能を有効にするには、/Qvec-report[0-5] (Windows)、-vec-report[0-5] (Linux & Mac OS X) オプションを追加してコンパイラーを起動します。オプションの引数には次の意味があります。
引数 | 引数の意味 |
---|---|
0 | 診断情報を表示しません。 |
1 | ベクトル化されたソース行を表示します。(デフォルトは 1 です) |
2 | ベクトル化されたソース行とベクトル化されなかったソース行を表示します。 |
3 | ベクトル化されたソース行とベクトル化されなかったソース行に加え、検出された依存関係や想定される依存関係を表示します。 |
4 | ベクトル化されなかったソース行を表示します。 |
5 | ベクトル化されなかったソース行とその理由を表示します。 |
例えば次のようなソースはベクトル化向きのコードですが、レポートオプションなしではコンパイラーはベクトル化に関する情報は出力しません。
for(i = 0; i < MAX; i++){ A[i] += B[i] * C[i]; }
そこで、実際にベクトル化されているか確かめるため、/Qvec-report オプションを引数なしで指定してみます。引数なしでは「1」がデフォルト解釈され、次のようにベクトル化されたソース行が表示されます。
C:\sample.cpp(行番号): (col. 2) remark: ループがベクトル化されました。メッセージ中の(行番号)には、実際のソースコードの行番号が表示されます。上記のソースでは for 文に該当する場所です。
実はこのオプションの真価は、ベクトル化できた場所を知ることより、ベクトル化できなかった場所を知ることで発揮されます。ベクトル化できなかった場所とは、ベクトル化に向いているソースコードではあるが、何らかの原因でそれができなかったソースコード領域を指します。つまり、皆さんがその場所を見て原因を排除できれば、コンパイラーはベクトル化できることを意味します。
ベクトル化できない場合
/Qvec-report オプションを指定して、前述の「ベクトル化されました」というメッセージが見られなかった場合や、ベクトル化の可能性を調査したい場合、レポートオプションに 2 もしくは 4 の引数を追加して再度コンパイルを行ってください。コンパイラーはベクトル化の可能性のあるソース行をレポートします。
// サンプルコード: mul.c #include "mul.h" void matvec(float a[][COLWIDTH], float b[], float x[]){ int i, j, sz1 = size1; sz2 = size2; for (i = 0; i < sz1; i++) { // 5 行目 b[i] = 0; for (j = 0;j < sz2; j++) { // 7 行目 b[i] += a[i][j] * x[j]; // 8 行目 } } }
上記のサンプルコードを icl mul.c /c /Qvec-report でコンパイルしても、ベクトル化レポートは表示されません。 /Qvec-report2 でベクトル化できなかった場所があるか調査してみます。このサンプルでは次のメッセージが表示されました。
C:\mul.c(5): (col. 2) remark: ループはベクトル化されませんでした: 内部ループではありません。C:\mul.c(7): (col. 3) remark: ループはベクトル化されませんでした: ベクトル依存関係が存在しています。
① ループはベクトル化されませんでした: 内部ループではありません。
最初のメッセージは、5 行目の for ループが外部ループであるため、ベクトル化できないことを示しています。この例のように for ループが入れ子になって、それぞれのループカウンター(ここでは、i と j )をメモリーアクセスのインデックスとして利用するような場合、インテル® コンパイラーは内側のループをベクトル化しようとします。
8 行目の b[i] += a[i][j] * x[j] を見ると、内側のインデックス j は、a の一次元と x の配列で連続したメモリーアクセスに利用されており、本来ベクトル化に適した処理です。このメッセージは無視しても構いません。ただし、5 行目と 7 行目のループが逆である場合は、内側のループは連続したメモリーアクセスができないためループ変換を検討してください。
② ループはベクトル化されませんでした: ベクトル依存関係が存在しています。2番目のメッセージは、7 行目の for ループを基にベクトル化を行うと、依存性もしくはその可能性があることを示しています。8 行目の b[i] += a[i][j] * x[j] を見ると、インデックス j を利用したメモリーアクセスには一見依存性は無いように見えます。
ここで注目してほしいのは、a、b、x の各配列は関数へのポインター引数として渡されていることです。コンパイラーは、書き込み先の配列 b とデータを読み込んでいる a または x の配列が、同じアドレスを持っていないことを知ることができません。このような場合、インテル® コンパイラーは安全のため、依存性があると判断しベクトル化を行わないのです。
これを解決するには、コンパイラーにアドレスのオーバーラップ(エリアシング)がないことを教えてあげればいいわけですが、この場合次の4つの方法があります。
a) /Qipo (Windows)、-ipo (Linux & Mac OS X) を利用する
ipo (プロシージャ間の最適化) オプションでは、ファイル間にまたがった解析を可能にします。Visual C++ の WPO に似た機能ですが、さらに強力です。ipo によりコンパイラーは、関数 matvec を呼び出している main ルーチンがどのように a、b、そして x の配列を定義して引数として渡しているか判断できます。そのため、mul.c 内の 7 行目のループはベクトル化できるようになります。 ipo を指定すると強力にインライン展開が行われるため、ベクトル化された場所を解り易くするため /Ob0 でインライン展開を無効にしています。
ipo オプションは様々な最適化に影響を与えます。ソースコードを解析する必要もないので、まずは ipo で生成したバイナリーで性能評価を行うことをお勧めします。
b) /Qno-alias-args (Windows)、-fargument-noalias (Linux & Mac OS X) を利用する
no-alias-args は、関数呼び出し時の引数ポインターにエリアス(別名)が無いことを強制します。効果は ipo と同じですが、no-alias-args ではデータ宣言を確認しないため、データの境界配置が正しいかどうかは未定です。一部のプロセッサーでは、SSE命令の高速なメモリーアクセスには、データの16バイトアライメントが要求されます。
c) 依存性がないことを示す #pragma ivdep を挿入する
ivdep はベクトル化の依存性をすべて無視することをコンパイラーに指示する pragma です。この例のエリアスに限らず、データ依存などすべての依存性を無視します。上記と同じくアライメントなどのセマンティクスに従っていることが求められます。7 行目のループの前に、#pragma ivdep を挿入します。
#pragma ivdep for (j = 0;j < sz2; j++) { // 7 行目 b[i] += a[i][j] * x[j]; // 8 行目 }
d) restrict キーワードを追加する
C 言語では c99 仕様でポインターが同じ場所を示さないことを指示する restrict 修飾子がサポートされています。この例では、配列 b が他のポインターとエリアスが無いことを指示するため、関数 matvec の引数部に restrict 修飾子を追加します。
void matvec(float a[][COLWIDTH], float b[restrict], float x[])
そして、コンパイル時に restrict 修飾子を有効にする /Qrestrict (-restrict) オプションを追加してコンパイルします。最新のインテル® C++ コンパイラー バージョン 12 では、デフォルトで c99 機能が有効になっていますが、以前のバージョンのコンパイラーを利用する場合、 /Qstd=c99 (-std=c99) を同時に利用しなければならないことがあります。
上記の解決策を適用して生成したコードの実行時間は次のようになります。サンプルプログラムの実行時間検証に利用したシステムは、インテル® Core™i7 965 (3.2GHz/L3 8MB/HT OFF) + 12GB メモリー上で動作する Windows 7 Ultimate 64 bit です。
サンプルの実行時間を比較デフォルト 8.75 秒
/Qipo /Ob0 5.02 秒
/Qipo 2.71 秒
/Qno-alias-args 5.33 秒
#pragma ivdep を追加 5.33 秒
restrict を利用 5.25 秒
このサンプルは、単精度浮動小数点データを処理しているため、ベクトル化により理論上期待できるスピードアップは 4 倍です。デフォルトコンパイルしたバイナリーは、約 8.75 秒でした。各解決策による実行時間は、デフォルトの 1.6 倍前後に性能を改善できています。このサンプルは、main ルーチンが 1 千万回 matvec 関数を呼び出して時間計測を行っています。そのため、/Ob0 を指定しない ipo 環境では、関数呼び出しのオーバーヘッドも軽減され、その他幾つかの最適化も有効となり約 3.2 倍のスピードアップとなっています。
ガイド機能
バージョン 12 のインテル® コンパイラーでは、新たにガイド付き最適化 (/Qguide オプション) がサポートされています。この機能とレポート機能の違いは、レポートでは問題点は報告してくれますが、問題は開発者自身が考えて直さなければいけません。ガイドによる最適化では、どのように問題に対処すべきか、コンパイラーがアシストメッセージを出力してくれます。
前述のサンプルコード、mul.c を icl mul.c /c /Qguide でコンパイルすると次のメッセージが表示されます。前述の (b) と (c) の解決策が指示されています。このように、開発者が行うべきことを教えてくれるため、今後非常に有効な機能であると思います。
C:\mul.c(7): remark #30536: (LOOP) 必要に応じて、-Qno-alias-args オプションを追加してコンパイラーによる型ベースの一義化解析を向上できます (オプションはコンパイル全体に適用されます)。これにより、行 7 のループがベクトル化され最適化が向上します。[確認] コンパイル全体でこのオプションのセマンティクスに沿っていることを確認してください。[別の方法] ルーチン “matvec” のすべてのポインター型の引数に “restrict” キーワードを追加することで同様の効果が得られます。これにより、行 7 のループがベクトル化され最適化が向上します。[確認] “restrict” ポインター修飾子のセマンティクスに沿っていることを確認してください。ルーチンにおいて、そのポインターによってアクセスされるすべてのデータは、ほかのポインターからはアクセスできません。 このコンパイルセッションで出力されたアドバイス・メッセージの数: 1。 GAP レポート記録終了
このガイド付き最適化は、並列化にも役立ちます。起動時のオプションに /Qparallel を同時に指定すると、並列化に関するアドバイスも生成できます。
この記事で利用したサンプルコードは、ここからダウンロードできます。Windows 以外のシステムでもコンパイルできます。
次回は、さらに多くのコードパターンでベクトル化の仕組みと活用方法を紹介する予定です。
この記事は 2011年 6月 3日に書いていますが、公開は 13日頃の予定です。何かご要望などありましたら、iSUS 事務局 (info@isus.jp) へお送りください。連載終了前にお答えできるものは、記事中で紹介していこうと思っています。
第1回 第2回 第3回 第4回 第5回 最終回