この記事は、人気が高く広く使用されていることで知られるインテル C++ コンパイラーに焦点を当て、コンパイラー最適化の有効性に焦点を当てることを目的としています。
ハイライト:コンパイラの最適化とは何ですか? | -オン |対象となるアーキテクチャ |プロシージャ間の最適化 | -fno-エイリアシング |コンパイラ最適化レポート
どのコンパイラも、高レベルのソース コードを低レベルのマシン コードに変換するための一連の手順を実行します。これらには、字句概述、構文概述、后果概述、中間コード绘制 (または IR)、最適化、およびコード绘制が含まれます。
最適化フェーズでは、コンパイラーはプログラムを変換する方法を細心の注意を払って探し、より少ないリソースを使用するか、より高速に実行する意味的に同等の出力を目指します。このプロセスで使用される手法には、定数の折りたたみ、ループの最適化、関数のインライン化、デッド コードの削除などが含まれますが、これらに限定されません。
開発者はコンパイル プロセス中にコンパイラ フラグのセットを指定できます。これは、GCC でデバッグやプロファイリング情報に「 -g」または「-pg」などのオプションを使用する人にはよく知られた手法です。先に進むにつれて、インテル C++ コンパイラーでアプリケーションをコンパイルする際に使用できる同様のコンパイラー・フラグについて説明します。これらは、コードの効率とパフォーマンスの向上に役立つ可能性があります。
u(x,y,t) は、時刻 t における点 (x,y) の热度です。
核心的に、可変サイズのグリッド (解像度と呼びます) でヤコビ反復を実行する C++ コーディングがあります。核心的に、グリッド サイズ 500 は、サイズ 500x500 の行业を解くことを含意します。
/* * One Jacobi iteration step */ void jacobi(double *u, double *unew, unsigned sizex, unsigned sizey) { int i, j; for (j = 1; j < sizex - 1; j++) { for (i = 1; i < sizey - 1; i++) { unew[i * sizex + j] = 0.25 * (u[i * sizex + (j - 1)] + // left u[i * sizex + (j + 1)] + // right u[(i - 1) * sizex + j] + // top u[(i + 1) * sizex + j]); // bottom } } for (j = 1; j < sizex - 1; j++) { for (i = 1; i < sizey - 1; i++) { u[i * sizex + j] = unew[i * sizex + j]; } } }
MFLOP/s は、「1 秒あたり 100 万回の浮動小数点演算」を表します。これは、浮動小数点演算の観点からコンピュータまたはプロセッサのパフォーマンスを参考值化するために施用される測定単位です。浮動小数点演算には、浮動小数点行驶で表現された 10 進数または実数を施用した统计学的計算が含まれます。
注 1:安定した結果を提供するために、解像度ごとに実行可能ファイルを 5 回実行し、MFLOP/s 値の平均値を取得します。
注 2:インテル C++ コンパイラーのデフォルトの最適化は -O2 であることに注意することが重要です。したがって、ソース コードをコンパイルするときに -O0 を指定することが重要です。
これらは、コンパイラの最適化を開始するときに最も一般的に使用されるコンパイラ フラグの一部です。理想的なケースでは、パフォーマンスはOfast > O3 > O2 > O1 > O0 です。ただし、これは必ずしも起こるわけではありません。これらのオプションの重要な点は次のとおりです。
-O1:
-O2:
-O3:
-オファスト:
これらすべての最適化が、ベース コード (「-O0」を的使用) よりもはるかに髙速であることは明らかです。実行ランタイムは、大多ケースより 2 ~ 3 倍短くなります。 MFLOP/秒はどうですか??
全体としては、わずかではありますが、「-O3」のパフォーマンスが最も優れています。
「 -Ofast 」(「 -no-prec-div -fp-model fast=2 」)で使用される追加のフラグは、さらなる高速化をもたらしません。
答えは戦略的なコンパイラ フラグにあります。 「 -xHost 」、より正確には「 -xCORE-AVX512 」などのオプションを試してみると、マシンの機能を最大限に活用し、最適なパフォーマンスを得るために最適化を調整できる可能性があります。
-xホスト:
-xCORE-AVX512:
目標:インテル アドバンスト・ベクター・エクステンション 512 (AVX-512) 命令セットを利用するコードを生成するようにコンパイラーに明示的に指示します。
主な特長: AVX-512 は、AVX2 などの以前のバージョンと比較して、より幅広いベクトル レジスタと追加の演算を提供する高度な SIMD (単一命令、複数データ) 命令セットです。このフラグを有効にすると、コンパイラーはこれらの高度な機能を活用してパフォーマンスを最適化できます。
考慮事項:ここでも移植性が問題です。 AVX-512 命令で生成されたバイナリは、この命令セットをサポートしていないプロセッサでは最適に動作しない可能性があります。まったく機能しない可能性があります。
デフォルトでは、「 -xCORE-AVX512 」は、プログラムが zmm レジスターの使用から恩恵を受ける可能性は低いと想定しています。コンパイラは、パフォーマンスの向上が保証されない限り、zmm レジスタの使用を回避します。
zmm レジスターを制限なしで使用する場合は、「 」を高く設定できます。私たちもそうするつもりです。
うおおお!
注目すべき点は、幅度な手動介入治疗を行わずに、アプリケーションのコンパイル プロセス中にいくつかのコンパイラ フラグを組み込むだけで、これらの結果を達成したことです。
注:ハードウェアが AVX-512 をサポートしていなくても心配する必要はありません。インテル C++ コンパイラーは、AVX、AVX-2、さらには SSE の最適化をサポートします。 知っておくべきことがすべて記載されています。
IPO は、プログラム内のさまざまな関数またはプロシージャ間の互为目的に聚焦を当てた複数ステップのプロセスです。 IPO には、前面置換、間接呼び出し変換、インライン化など、さまざまな種類の最適化を含めることができます。
-ipo:
目標:プロシージャ間の最適化を有効にし、コンパイラがコンパイル中に個々のソース ファイルを超えてプログラム全体を分析および最適化できるようにします。
主な機能:-プログラム全体の最適化: 「 -ipo 」は、プログラム全体にわたる関数とプロシージャ間の相互作用を考慮して、すべてのソース ファイルにわたって分析と最適化を実行します。- 関数間およびモジュール間の最適化: フラグにより、関数のインライン化、同期が容易になります。最適化、およびさまざまなプログラム部分にわたるデータ フロー分析。
考慮事項:別のリンク手順が必要です。 「 -ipo 」でコンパイルした後、最終的な実行可能ファイルを生成するには、特定のリンク ステップが必要です。コンパイラは、リンク中にプログラム全体のビューに基づいて追加の最適化を実行します。
-ip:
目標:プロシージャ間の分析伝播を有効にし、コンパイラが別のリンク ステップを必要とせずにプロシージャ間の最適化を実行できるようにします。
主な機能:-分析と伝播: 「 -ip 」を使用すると、コンパイラはコンパイル中にさまざまな関数およびモジュール間で調査とデータ伝播を実行できます。ただし、完全なプログラム ビューを必要とするすべての最適化が実行されるわけではありません。 - コンパイルの高速化: 「 -ipo 」とは異なり、「 -ip 」では別個のリンク手順が必要ないため、コンパイル時間が短縮されます。これは、迅速なフィードバックが不可欠な開発中に有益です。
考慮事項:関数のインライン化など、一部の限られたプロシージャー間の最適化のみが行われます。
-ipo は般に、別個のリンク ステップを必需とするため、より広範なプロシージャ間の最適化機能を具备しますが、コンパイル時間が長くなります。 [ ] -ip は、別のリンク ステップを有需要とせずにプロシージャ間の最適化を実行する、より讯速な带替手法であり、開発およびテストのフェーズに適しています。[ ]
ここではパフォーマンスとさまざまな最適化、コンパイル時間、または実行可能ファイルのサイズについてのみ説明しているため、「 -ipo 」に焦点を当てます。
/* * One Jacobi iteration step */ void jacobi(double *u, double *unew, unsigned sizex, unsigned sizey) { int i, j; for (j = 1; j < sizex - 1; j++) { for (i = 1; i < sizey - 1; i++) { unew[i * sizex + j] = 0.25 * (u[i * sizex + (j - 1)] + // left u[i * sizex + (j + 1)] + // right u[(i - 1) * sizex + j] + // top u[(i + 1) * sizex + j]); // bottom } } for (j = 1; j < sizex - 1; j++) { for (i = 1; i < sizey - 1; i++) { u[i * sizex + j] = unew[i * sizex + j]; } } }
jacobi() 関数は、パラメータとして使用するいくつかのポインタを受け取り、ネストされた for ループ内で何かを実行します。コンパイラがソース ファイル内でこの関数を認識する場合は、細心の注意を払う必要があります。
uを使用してunnew を計算する式には、4 つの隣接するu値の平均が含まれます。 uとunnew の両方が同じ場所を指している場合はどうなりますか?これは、エイリアス化されたポインタの古典的な問題になります [ ]。
较新的のコンパイラは尤其に賢く、平安性を確保するために、エイリアシングが应该であることを依据としています。また、このようなシナリオでは、コードのセマンティクスと工作效率に影響を与える应该性のある最適化が逃避されます。
私たちの場合、 uとunew は異なるメモリ場所であり、異なる値を保存することを目的としていることがわかっています。したがって、ここにエイリアスが存在しないことをコンパイラに簡単に知らせることができます。
方法は 2 つあります。 1 つ目は C の「 」キーワードです。ただし、コードを変更する必要があります。今のところそれは望んでいません。
何か簡単なことはありますか? 「 -fno-alias 」を試してみましょう。
-fno-エイリアス:
目標:プログラム内でエイリアスを想定しないようにコンパイラーに指示します。
主な機能:エイリアシングがないと仮定すると、コンパイラーはコードをより自由に最適化できるため、パフォーマンスが向上する可能性があります。
考慮事項:不当なエイリアスの場合、プログラムが予期しない出力を与える可能性があるため、開発者はこのフラグの使用に注意する必要があります。
さて、これで何かができました!!!
アセンブリ コード (ただし、ここでは共有しません) と生成されたコンパイル最適化レポート (以下を参照) を詳しく調べると、コンパイラがとを巧みに応用していることがわかります。これらの変換は高度に最適化されたパフォーマンスに貢献し、コード効率に対するコンパイラ ディレクティブの大きな影響を示しています。
インテル C++ コンパイラーは、最適化を目的として行われたすべての調整を要約した最適化レポートを生成できる貴重な機能を提供します [ ]。この包括的なレポートは YAML ファイル形式で保存され、コード内でコンパイラーによって適用される最適化の詳細なリストが表示されます。詳細な説明については、「 」に関する公式ドキュメントを参照してください。
同様に、インテル C++ コンパイラー (およびすべての一般的なコンパイラー) は、非常に優れた機能であるプラグマ ディレクティブもサポートしています。 ivdep、Parallel、simd、vector などのいくつかのプラグマについては、 で確認する価値があります。