最適化の推奨事項
このドキュメントでは、Visual Studio での C++ プログラムの最適化に関するベスト プラクティスについて説明します。
コンパイラとリンカーのオプション
ガイド付き最適化のプロファイル
Visual Studio では、"ガイド付き最適化のプロファイル" (PGO) がサポートされています。 この最適化では、後でアプリケーションの最適化を行う際に、インストルメント化されたバージョンのアプリケーションのトレーニング実行で収集されたプロファイル データを使用します。 PGO を使用すると時間がかかる場合があるため、すべての状況に適しているとは言えません。しかし、製品の最終リリース ビルドには PGO を使用することをお勧めします。 詳細については、「ガイド付き最適化のプロファイル」を参照してください。
さらに、"プログラム全体の最適化" (リンク時のコード生成)、および /O1
と /O2
による最適化が改良されました。 通常、これらのオプションのいずれかを指定してコンパイルしたアプリケーションの処理速度は、以前のコンパイラでコンパイルした同じアプリケーションよりも速くなります。
詳細については、「/GL
(プログラム全体の最適化)」と「/O1
、/O2
(サイズの最小化、速度の最速化)」を参照してください。
使用する最適化のレベル
可能な限り、最終リリース ビルドは、PGO を使用してコンパイルする必要があります。 インストルメント化されたビルドを実行するにはインフラストラクチャが不十分であることや、シナリオにアクセスできないことが原因で、PGO を使用してビルドできない場合は、プログラム全体の最適化を使用してビルドすることをお勧めします。
/Gy
スイッチも非常に便利です。 このスイッチを使用すると、関数ごとに個別の COMDAT が生成されます。その結果、参照されていない COMDAT を取り除いたり COMDAT を圧縮したりでき、リンカーの柔軟性が向上します。 /Gy
を使用する唯一の欠点は、デバッグ時に問題が発生する可能性があることです。 重要な影響を受けるわけではないので、通常は、このスイッチを使用することをお勧めします。 詳細については、「/Gy
(関数レベルのリンクの有効化)」を参照してください。
64 ビット環境でリンクする場合は、/OPT:REF,ICF
リンカー オプションを使用することをお勧めします。また、32 ビット環境の場合は、/OPT:REF
をお勧めします。 詳細については、「/OPT (最適化)」を参照してください。
また、デバッグ シンボルを生成することを強くお勧めします。これは、最適化されたリリース ビルドの場合も同様です。 それにより、生成されたコードが影響を受けることはなく、必要に応じてアプリケーションを非常に簡単にデバッグできます。
浮動小数点スイッチ
/Op
コンパイラ オプションがなくなり、浮動小数点の最適化を処理する次の 4 つのコンパイラ オプションが追加されました。
オプション | 説明 |
---|---|
/fp:precise |
既定で推奨されているオプションです。ほとんどの場合は、これを使用します。 |
/fp:fast |
パフォーマンスが最も重要視される場合 (ゲームなど) に推奨されます。 このオプションを使用すると、パフォーマンスが最も高速になります。 |
/fp:strict |
浮動小数点の例外に関して厳密な対応が必要な場合や、IEEE に沿った動作が必要な場合に推奨されます。 このオプションを使用すると、パフォーマンスが最も低速になります。 |
/fp:except[-] |
/fp:strict または /fp:precise とは同時に使用できますが、/fp:fast と同時に使用することはできません。 |
詳細については、「/fp
(浮動小数点の動作の指定)」を参照してください。
最適化の declspec
このセクションでは、パフォーマンスを改善するためにプログラム内で使用できる、__declspec(restrict)
および __declspec(noalias)
という 2 つの declspec について説明します。
restrict
declspec は、__declspec(restrict) void *malloc(size_t size);
など、ポインターを返す関数宣言のみに適用できます。
restrict
declspec は、エイリアスを使用しないポインターを返す関数で使用されます。 このキーワードは、現在のプログラムで既に使用されているポインター値を返さないため、解放済みのメモリを使用するなどの無効な操作を行う場合を除いて、C ランタイム ライブラリの malloc
の実装に使用できます。
restrict
declspec は、コンパイラが最適化を実行するための詳細情報をコンパイラに伝えます。 コンパイラにとって最も困難なことの 1 つは、どのポインターが他のポインターのエイリアスとなっているかを判断することですが、その際、この詳細情報はコンパイラにとって非常に役立ちます。
この情報はコンパイラでは前提とされ、コンパイラが検証することはありません。 プログラムでこの restrict
declspec を不適切に使用すると、プログラムが不正な動作をするおそれがあります。
詳細については、restrict
を参照してください。
また、noalias
declspec も関数だけに適用され、その関数が準純粋関数であることを示します。 準純粋関数は、ローカル、引数、引数の第 1 レベルの間接参照だけを参照または変更する関数です。 この declspec はコンパイラでは前提とされます。準純粋関数がグローバル、またはポインター引数の第 2 レベルの間接指定を参照する場合、コンパイラが生成するコードはアプリケーションを中断させる可能性があります。
詳細については、noalias
を参照してください。
最適化のプラグマ
コードを最適化する際に有用なプラグマもいくつかあります。 最初に、#pragma optimize
について説明します。
#pragma optimize("{opt-list}", on | off)
このプラグマを使用すると、関数ごとに特定の最適化レベルを設定できます。 このプラグマは、特定の関数を最適化してコンパイルするとアプリケーションがクラッシュするという特殊な状況に適しています。 このプラグマを使用すると、1 つの関数の最適化を無効にできます。
#pragma optimize("", off)
int myFunc() {...}
#pragma optimize("", on)
詳細については、optimize
を参照してください。
コンパイラで実行する最適化のうち、最も重要な最適化の 1 つにインライン展開があります。ここでは、インライン展開の動作を変更できるプラグマをいくつか紹介します。
#pragma inline_recursion
は、アプリケーションで再帰呼び出しをインライン展開できるようにするかどうかを指定する場合に役立ちます。 既定では、インライン展開は無効になっています。 関数が小さく、再帰の数が少ない場合は、このプラグマを有効にできます。 詳細については、inline_recursion
を参照してください。
インライン展開の深さを制限する場合に役立つ別のプラグマとして、#pragma inline_depth
があります。 これは通常、プログラムまたは関数のサイズを制限する場合に役立ちます。 詳細については、inline_depth
を参照してください。
__restrict
および __assume
Visual Studio には、パフォーマンスの向上に使用できるキーワードとして __restrict および __assume があります。
まず、__restrict
と __declspec(restrict)
が異なるものであることに注意する必要があります。 この 2 つのキーワードは多少関連がありますが、意味が異なります。 __restrict
は、const
や volatile
のような型修飾子ですが、ポインター型に対してのみ使用できます。
__restrict
によって変更されたポインターは、"__restrict ポインター" と呼ばれます。 __restrict ポインターは、__restrict ポインターを介してのみアクセスできるポインターです。 つまり、別のポインターを使用して、__restrict ポインターが指すデータにアクセスすることはできません。
__restrict
は Microsoft C++ オプティマイザーにとって強力なツールになりますが、慎重に使用する必要があります。 誤って使用すると、オプティマイザーで最適化を実行した結果としてアプリケーションが破壊されることがあります。
__assume
を使用すると、開発者はコンパイラに対して、何らか変数の値について前提を設定するように指示できます。
たとえば、__assume(a < 5);
とすると、そのコード行では a
変数の値が 5 未満であることがオプティマイザーに伝えられます。 これも、コンパイラにとっての前提になります。 プログラム内のこの時点で a
が実際には 6 である場合、コンパイラでプログラムを最適化すると、意図したとおりに動作しない可能性があります。 __assume
は、switch ステートメントや条件式の前で使用するのが最も効果的です。
__assume
には、いくつかの制約があります。 まず、__restrict
と同様に、このキーワードは提案にすぎないため、コンパイラに無視される可能性があります。 また、__assume
は、現在、変数を定数と比較する場合にのみ機能します。 assume(a < b) など、シンボルどうしの不等式には応用できません。
組み込みのサポート
組み込みとは、呼び出しに関する情報がコンパイラに組み込まれている関数呼び出しです。コンパイラはライブラリ内の関数を呼び出すのではなく、その関数のコードを出力します。 ヘッダー ファイル <intrin.h> には、サポートされている各ハードウェア プラットフォームに使用できるすべての組み込みが含まれています。
組み込みにより、プログラマは、アセンブリを使用しなくても深い部分までコーディングできます。 組み込みを使用することには、いくつかの利点があります。
使用しているコードの移植性が高まります。 組み込みのいくつかは、複数の CPU アーキテクチャで使用できます。
コードは依然として C/C++ で記述されているため、簡単に理解できます。
コードはコンパイラの最適化による恩恵を受けることができます。 コンパイラが進歩すると、組み込みのコード生成も進歩します。
詳細については、「コンパイラの組み込み」を参照してください。
例外
例外を使用すると、それによってパフォーマンスに影響があります。 コンパイラによる特定の最適化を防ぐ try ブロックを使用する場合に、いくつかの制限が導入されました。 x86 プラットフォームで try ブロックを使用すると、コードの実行時に追加のステータス情報を生成する必要があるために、パフォーマンスがさらに低下します。 64 ビット プラットフォームでは、try ブロックによってパフォーマンスが大きく低下することはありませんが、例外がスローされると、ハンドラーの検出とスタックのアンワインドの処理に負荷がかかります。
したがって、try ブロックと catch ブロックは、本当に必要である場合を除いて使用しないことをお勧めします。 例外を使用する必要がある場合は、できるだけ同期例外を使用してください。 詳細については、「 Structured Exception Handling (C/C++)」を参照してください。
最後に、例外をスローするのは、例外の場合だけにしてください。 通常の制御フローに例外を使用すると、パフォーマンスが低下します。