Microsoft Visual C++ 2005 での PGO
Kang Su Gatlin
Microsoft Corporation
March 2004
適用対象:
Microsoft® Visual C++® 2005
概要: アプリケーションを顧客の実際の状況に合わせる新しい強力な機能である、Microsoft Visual C++ 2005 (以前は Visual C++ "Whidbey" と呼ばれていました) のPGO (Profile-Guided Optimization) について説明します。 実際の環境で、パフォーマンスが 20% 以上向上することはあまりありません。
目次
はじめに
従来の C++ コンパイラの動作
リンク時のコード生成によるプログラム全体の最適化
PGO (Profile Guided Optimization)
その他の PGO ツール
PGO と Visual Studio IDE
PGO 使用のヒント
まとめ
はじめに
C++ でプログラミングを行う理由はいくつかあり、最も重要な理由の 1 つは驚くべきパフォーマンスが得られることです。 Microsoft® Visual C++® 2005 のリリースでは、従来のすべての最適化方法から高いパフォーマンスを得られるだけでなく、新しい技術が追加されて、ユーザーがアプリケーションからさらなるパフォーマンスを引き出せるようになりました。 この記事では、ユーザーが PGO (Profile-Guided Optimization) を使用してこの驚くべきパフォーマンスを実現する方法について説明します。
従来の C++ コンパイラの動作
PGO について完全に理解するために、まず、実行する最適化の種類を従来のコンパイラが決定する方法について説明します。
従来のコンパイラでは、静的なソース ファイルに基づいて最適化が実行されます。 つまり、ソース ファイルのテキストは分析されますが、ソース コードからは直接取得できない潜在的なユーザー入力に関する情報は使用されません。 たとえば、.cpp ファイルの次の関数について考えてみましょう。
int setArray(int a, int *array)
{
for(int x = 0; x < a; ++x)
array[x] = 0;
return x;
}
このファイルからは、コンパイラが "a" に入力される値についての情報を得ることはできず (しかも、この値は int 型となる必要があります)、配列の標準的な配置についての情報を得ることもできません。
このコンパイラおよびリンカ モデルが特に悪いわけではありませんが、最適化を行う 2 つの主要な機会を逃しています。 1 つ目に、すべてのソース ファイルを同時に分析することから得られる情報を活用していません。2 つ目に、アプリケーションの正常な動作またはプロファイルを使用した動作に基づいて最適化を行っていません。 WPO および PGO を使用すると、それらの両方を実行できます。
リンク時のコード生成によるプログラム全体の最適化
Itanium® コンパイラの最近の全バージョンを含む Visual C++ 7.0 以降のバージョンの Visual C++ では、リンク時コード生成 (LTCG) と呼ばれるメカニズムがサポートされてきました。 LTCG については、Matt Pietrek による良い記事が「Under the Hood column (May 2002) (英語)」にあり、MSDN から無料で参照できるため、このセクションでは詳しく説明しません。 ここでは、基本的なことだけを説明します。
LTCG は、コンパイラがすべてのソース ファイルを単一の変換単位として効率的にコンパイルできるようにする技術です。 これは、次の 2 つの段階で実行されます。
- コンパイラがソース ファイルをコンパイルし、その結果を、通常のオブジェクト ファイルではなく、生成された .obj ファイルに中間言語 (IL) として出力します。 この IL は、MSIL (これは、Microsoft® .NET Framework により使用されます) と同じものではない点に注意してください。
- リンカが /LTCG により呼び出されると、リンカは実際にバックエンドを呼び出して、WPO でコンパイルされたすべてのコードをコンパイルします。 WPO .obj ファイルからのすべての IL が集められ、完全なプログラムのコール グラフを生成できるようになります。 このコール グラフから、コンパイラのバックエンドおよびリンカはプログラム全体をコンパイルし、それを実行可能なイメージにリンクします。
WPO では、コンパイラがプログラム全体の構造に関する詳細な情報を取得するようになりました。 このため、ある種の最適化をより効率的に実行できます。 たとえば、従来のコンパイルおよびリンクを行う際、コンパイラは関数をソース ファイル foo.cpp からソース ファイル bar.cpp にインラインできませんでした。bar.cpp をコンパイルする際、コンパイラは foo.cpp に関する情報を持っていません。WPO では、コンパイラが bar.cpp と foo.cpp の両方を (IL 形式で) 取得可能なため、通常は不可能な最適化 (変換単位間のインラインなど) を実行できるようになりました。
アプリケーションをコンパイルして、LTCG を使用する方法には、次の 2 つの手順があります。
まず、"プログラム全体の最適化" コンパイラ スイッチ (/GL) を使用して、ソース コードをコンパイルします。
cl.exe /GL /O2 /c test.cpp foo.cpp
次に、/LTCG スイッチを使用して、プログラム内のすべてのオブジェクト ファイルをリンクします。
link /LTCG test.obj foo.obj /out:test.exe
以上の手順で終わりです。 これで、生成された実行可能ファイルを実行できるようになり、多くの場合高速化します。 LTCG の使用には多くのメリットがありますが、費用をかけずに使うことはできません。 コンパイルおよびリンク時のメモリ要件が高くなります。 これは、すべての IL をアドレス指定する必要があり、IL は数十または数百のコンパイル単位となる可能性があるためです。 これにより、プロジェクトのビルドに必要なメモリ要件が高くなり、さらにはビルドの合計時間が増加する可能性があります。
PGO (Profile Guided Optimization)
LTCG には、明らかにいくらかのパフォーマンス メリットがありますが、それはまだアプリケーションのパフォーマンス向上の初歩的な段階に過ぎません。 LTCG と組み合わせて使用される別の新しい技術によりさらにパフォーマンスが向上する可能性があり、多くの場合その向上は非常にはっきりと表れます。 この技術は、PGO (Profile-Guided Optimization) と呼ばれるものです。
PGO の背後にある考えは単純なものです。 実際の入力環境で実行可能ファイルまたは DLL を実行してプロファイルを生成します。プロファイルは、特定の実行可能ファイルの最適化されたコードを生成する点でコンパイラを支援するために使用されます (PGO は、アンマネージ実行可能ファイルまたは DLL の最適化に適用できますが、.NET またはマネージ イメージには適用されない点に注意してください。 この記事の残りの部分で、最適化イメージについて単に実行可能ファイルまたはアプリケーションに関するものとして言及しますが、情報は DLL にも同様に適用されます)。 実際はこれだけで説明できるのですが、さらに調べるとよく理解できます。
PGO アプリケーションの作成には、次の一般的な 3 つの段階があります。
- インストルメント コードにコンパイルします。
- インストルメント コードを試験実行します。
- 最適化コードに再コンパイルします。
これらの 3 つの各段階ついて、以下でさらに詳しく説明します。 図 1 は、この過程を図に表したものです。
図 1. PGO のビルド過程
インストルメント コードのコンパイル
最初の段階は、実行可能ファイルのインストルメント化です。 これを行うには、まず WPO (/GL) を使用してソース ファイルをコンパイルします。 その後、すべてのソース ファイルをアプリケーションから出力し、/LTCG:PGINSTRUMENT スイッチ (/LTCG:PGI という省略形を使用できます) を使用してそれらをリンクします。 アプリケーション上で全体として動作するために、すべてのファイルを /GL を使用して PGO 用にコンパイルする必要はありません。 PGO は、/GL を使用してコンパイルされたファイルをインストルメント化し、コンパイルされていないファイルはインストルメント化しません。
インストルメント化は、さまざまな種類のプローブをコードに戦略的に配置することにより行われます。 プローブの種類は、フロー情報を収集する種類と値情報を収集する種類との、2 つの非常に大まかな種類に分けることができます。 どれをどこで使用するかに関して詳細には説明しませんが、プローブを効率的に使用するためには大きな努力を払う必要があります。 さらに、インストルメント コードは、インストルメント化されてない同じ /O2 コードのように、/O2 で最適化されないことがある点に注意してください (ただし、インストルメント コードに配置するプローブの邪魔をせずに、できる限り多くの最適化を行います)。 このため、インストルメント コードと最適化しないコードとを組み合わせて使用すると、アプリケーションの動作が遅くなることが予想されます (もちろん、最適化するコードは、コード内のプローブを使用せずに最適化されます)。
/LTCG:PGI のリンクの結果、実行可能ファイルまたは DLL のどちらかと、PGO データベース ファイル (.PGD) が生成されます。 デフォルトでは、PGD ファイルの名前は生成された実行可能ファイルの名前となりますが、ユーザーが /PGD:ファイル名リンカ オプションを使用してリンクを行うと、PGD ファイルの名前を指定できます。
以下の表 1 には、左側の列の各手順後に生成されるファイルが一覧表示されています。 ファイルがまったく削除されない点に注意してください。
表 1. 各手順後に生成されるファイル
手順 | 生成されるファイル |
---|---|
コンパイルの開始時 | MyApp.cpp foo.cpp |
/c /GL によるコンパイル後 | MyApp.obj foo.obj |
/LTCG:PGI /PGD:MyApp.pgd /out:MyApp.inst.exe とのリンク後 | MyApp.inst.exe MyApp.pgd |
3 つのシナリオを使用したインストルメント アプリケーションの試験実行後 | MyApp1.pgc MyApp2.pgc MyApp3.pgc |
/LTCG:PGO./PGD:MyApp.pgd との再リンク後 | MyApp.opt.exe |
インストルメント コードの試験実行
インストルメント化された実行可能ファイルを作成したら、次の手順は実行可能ファイルの試験実行です。 これは、実際に使用される状況を反映するシナリオを使用して実行可能ファイルを実行することにより行います。 各シナリオの実行後は、PGO カウント ファイル (.PGC) が出力されます。 それらのファイルは .PGD ファイルと同じ名前になり、末尾に数字が付加されます ("1" から始まって実行ごとに大きくなります)。 特定のシナリオが使用に向いていないとユーザーが判断した場合、該当する .PGC ファイルを削除できます。
最適化されたコードのコンパイル
最後の手順として、実行可能ファイルを、実行したシナリオから収集されたプロファイル情報と再リンクします。 ここでアプリケーションをリンクする際、リンカ スイッチ /LTCG:PGOPTIMIZE (または /LTCG:PGO) を使用します。 これにより、生成されたプロファイル データを使用して、最適化された実行ファイルが作成されます。 この最適化に先立ち、リンカにより自動的に pgomgr が呼び出されます。デフォルトでは、名前が .PGD ファイルと一致する現在のディレクトリ内のすべての .PGC ファイルが、pgomgr により .PGD ファイルにマージされます。
- ソース コードの更新。 コンパイル済みのアプリケーションのソース コードが .PGD ファイルの生成後に変更された場合、/LTCG:PGO はそのまま /LTCG ビルドの実行状態に戻り、どのプロファイル情報も使用しなくなります。 では、インストルメント コードからかなりの量のプロファイルを生成しており、コードに小さな変更を加える必要のあることは理解していても、生成したプロファイルを再使用したい場合はどうればよいでしょうか。 この場合は、/LTCG:PGUPDATE (または /LTCG:PGU) を指定できます。 PGUPDATE により、リンカが元の .PGD ファイルを使用したまま、変更されたソース コードのコンパイルを行うことが可能になります。
PGO により実現できること
ここまでで PGO アプリケーションの生成方法について理解できたので、次に PGO により何を実現できるかを考えます。 どのような最適化を行うことができるでしょうか。 新しい最適化方法を発見して、より効率的なヒューリスティックを学習するため、ここでは部分的な一覧を示し、拡張のために現在行う最適化のセットについて説明します。
インライン。 前述のように、WPO によりアプリケーションは多くのインラインの機会が得られるようになります。 PGO を使用すると、この決定を行うのに役立つ情報が追加されます。 たとえば、以下の図 2、3、および 4 のコール グラフについて考えます。
図 2 では、a、foo、および bat がすべて bar を呼び出し、次に同様に baz を呼び出すのがわかります。
図 2. プログラムの元々のコール グラフ
図 3. PGO により取得された、呼び出し頻度の測定結果
図 4. 図 3 で取得されたプロファイルに基づいて最適化されたコール グラフ
**部分インライン。**次は、ほとんどのプログラマにとって、少なくとも部分的にはよく知られている最適化です。 使用頻度の高い多くの関数には、それほど頻度の高くない (ほとんど使用されないものもあります) 関数内のコードのパスが存在します。 以下の図 5 では、コードの紫色のセクションをインラインし、青色のセクションはそのままにします。
図 5. 紫色のノードはインラインされ、青色のノードはインラインされないことを示す制御フロー グラフ
**使用頻度の低いコードの分離。**プロファイル中に呼び出されないコード ブロック (使用頻度の低いコード) は、セクション セットの最後に移動されます。 このため、ワーキング セットのページは通常、プロファイル情報に従って実行される命令で構成されています。
図 6. 最適化レイアウトで、使用頻度の高い基本ブロックをまとめ、使用頻度の低いブロックを最後に移動する様子を示す制御フロー グラフ
**サイズと速度の最適化。**使用頻度の高い関数は速度を最適化でき、使用頻度の低い関数はサイズを最適化できます。 この最適化は、ちょうどよいバランスになる傾向があります。
**ブロック レイアウト。**この最適化では、関数内で最も使用頻度の高いパスを形成し、使用頻度の高いパスが空間的に近くに位置するようにそれらをレイアウトします。 これにより、命令キャッシュの利用率が増加し、ワーキング セットのサイズと使用されるページ数が減少します。
**仮想呼び出し予測。**仮想呼び出しは、V テーブル内でジャンプしてメソッドを呼び出すために負荷がかかります。 PGO を使用すると、コンパイラは仮想呼び出しの呼び出しサイトで予測を行い、予測したオブジェクトのメソッドを仮想呼び出しサイトにインラインできます。この決定を行うデータは、インストルメント アプリケーションを使用して収集されます。 最適化されたコードでは、インラインされた関数の周りのガードは、予測したオブジェクトの種類が生成されたオブジェクトに一致することを確認するチェックとなります。
以下の擬似コードは、基本クラス、生成された 2 つのクラス、および仮想関数を呼び出す関数を示しています。
class Base{ ... virtual void call(); } class Foo:Base{ ... void call(); } class Bar:Base{ ... void call(); } // これは、PGO に最適化される前の Func 関数です。 void Func(Base *A){ ... while(true) { ... A->call(); ... } }
以下のコードは、上記のコードを最適化した結果を示しており、動的な種類の "A" はほとんどの場合 Foo となります。
// これは、PGO に最適化された後の Func 関数です。 void Func(Base *A){ ... while(true) { ... if(type(A) == Foo:Base) { // inline of A->call(); } else A->call(); ... } }
DLL の使用
ここでは、PGO と DLL について簡単に説明します。 DLL の試験実行およびプロファイルは、実行可能ファイルを実行することにより行います。実行可能ファイルは、代表的なシナリオ セットの DLL をリンクします。 さらに、さまざまなシナリオで種々の実行可能ファイルを使用して、すべてのシナリオを単一の .PGD ファイルにマージできます。 PGO 技術では、現在のところスタティック ライブラリがサポートされていない点に注意してください。
全体的な効果
PGO の現在の実装は、実際の環境のパフォーマンスを向上する点で非常に効果があることがわかってきました。 たとえば、Microsoft® SQL Server などの実際の大きなアプリケーションでパフォーマンスが 30% 以上向上し、SPEC ベンチマークでは 4% から 15% が得られました (アーキテクチャに依存します)。 静的なコンパイルの最も効率的な設定 (リンク時コード生成) を使用した場合の PGO のパフォーマンス速度向上については、SPEC ベンチマークによる図 7 を参照してください。
図 7. 3 つのプラットフォームすべてについての、リンク時コード生成 (LTCG) 使用時の PGO による SPEC パフォーマンスの向上
その他の PGO ツール
Microsoft® Visual C++ での PGO サポートには、必要な作業を正確に実行するのに役立ついくつかのツールが含まれます。このセクションでは、それらの各ツールについて説明します。
pgomgr。pgomgr は、PGO により生成される .pgd ファイルの後処理を行うツールです (PSDK Itanium コンパイラ ユーザーの場合、Whidbey の pgomgr は以前の PSDK コンパイラの pgmerge と pgopt の一部を置き換えるものとなります。 これらの 2 つのツールは、その機能が包含されているために使用できなくなりました)。 .PGC ファイルは、PGO の最適化段階で使用される .PGD にマージする必要があります。 pgomgr ツールは、このマージを行います。 このステートメントの構文は次のとおりです。
pgomgr [options] [Profile-Count paths] <Profile-Database>
デフォルトでは、.PGC ファイルの存在するディレクトリで /LTCG:PGI が実行されると、それらの .PGC ファイルは、リンクされている program の .PGD ファイルに一致する場合はマージされます (このため、必ずしも pgomgr を使用する必要はありません)。 オプションの一覧は、次のとおりです。
- /?Gets help
- /helpSame as /?
- /clear 指定した pgd からすべてのマージ データを削除します。
- /detail プログラムの詳細な統計値を表示します。
- /merge[:n] 特定の PGC ファイルをマージします。オプションで整数の重み付けを使用します。
- /detail プログラムの統計値を表示します。
- /unique 関数の修飾名を表示します。
pgosweep。 pgosweep ツールは、PGO のインストルメント化を使用してビルドされたプログラムの実行を中断して、現在のカウントを新しい .PGC ファイルに書き込んだ後、実行時データ構造からカウントを消去します。 このプログラムには、意図された主な 2 つの用途があります。 1 つ目に、PGO は終わりのないコードで使用されます (たとえば、OS カーネルなどのコード)。 2 つ目に、プログラムの特定の部分に関する正確なプロファイル情報の取得に使用されます。 たとえば、アプリケーションの夜間シナリオをプロファイルしたくない場合、それらの状況で pogosweep を使用してシナリオのその部分から .PGC ファイルを削除することができます。
pogosweep の使用方法は次のとおりです。
pogosweep <instrumented image> <.PGC file to be created>
PGO と Visual Studio IDE
コマンドライン ツールは使いやすいツールですが、Microsoft® Visual Studio® の統合開発環境 (IDE) 内で作業している場合、Visual Studio IDE 内で PGO などの機能を完全に活用する必要が生じることがあります。 Visual Studio 2005 (以前は Visual Studio "Whidbey" と呼ばれていました) では、メニュー項目のセットにより PGO がサポートされます。プログラマは、メニュー項目を使用してインストルメント ビルドの実行、シナリオの実行、最適化ビルドの実行、または更新ビルドの実行を行うことができます。 インストルメント ビルド、最適化ビルド、および更新ビルドを実行すると、いずれの場合も出力として .exe または .dll ファイルが作成されます。 最適化ビルドと更新ビルドでは、.pgd ファイルが必要です。 この .pgd ファイルは、**[Run Profiling Scenario]**メニュー項目を実行することにより作成できます。
図 8. Visual Studio 2005 IDE における PGO サポートのスクリーンショット
PGO 使用のヒント
PGO を使用する際に役立つヒントをいくつか紹介します。
- プロファイル データの生成に使用されるシナリオは、アプリケーションが展開されたときに直面する実際のシナリオと似ています。 シナリオは、コード カバレッジを実行する試みではありません。
- 実際の環境とは異なるシナリオを使用して試験実行すると、PGO を使用しない場合よりもコードのパフォーマンスが悪化する可能性があります。
- 最適化されたコードは、インストルメント コードとは異なる名前にしてください。たとえば、app.opt.exe と app.inst.exe などです。このようにすれば、すべてのコードを再実行せずに、インストルメント アプリケーションを再実行してシナリオ プロファイルの設定を追加できます。
- 結果を調整するには、pgomgr の /clear オプションを使用して .PGD ファイルを消去します。
- 実行にかかる時間が異なる 2 つのシナリオがあるが、それらを等しく重み付けしたい場合、PGC ファイルに重み付けスイッチ (/merge:pgomgr での重み) を使用してそれらを調整できます。
- 速度スイッチを使用して、速度およびサイズのしきい値を変更できます。
- インラインしきい値スイッチは、特に注意して使用してください。 0 から 100 の値は、連続した値とはなりません。
まとめ
まとめとして、PGO アプリケーションを生成する基本手順は次のとおりです。
- PGO を実行するファイルで、プログラム全体の最適化 (/GL) を使用してコンパイルします。 ユーザーは、/GL を使用してコンパイルしないことにより、プログラム全体の最適化および PGO で最適化しないファイルを選択できます。
- /LTCG:PGINSTRUMENT を使用して、リンク時コード生成によってアプリケーションをリンクします。 これにより、インストルメント化された実行可能ファイルが生成されます。
- .pgc ファイルを生成するシナリオを使用して、アプリケーションを試験実行します。
- /LTCG:PGOPTIMIZE を使用してアプリケーションを再コンパイルします (ただし、リンカは呼び出します)。 これにより、プロファイル データに基づいて実行可能ファイルが最適化されます。
最終的な結果は、プログラムやライブラリが実行される実際の環境に合わせて最適化された、プログラムまたはライブラリになります。
執筆者紹介
Kang Su Gatlin は、Microsoft in the Visual C++ グループの Program Manager です。 UC San Diego で博士号を取得、 関心事はパフォーマンスの高い計算と最適化で、高速なコードの作成を心から楽しんでいます。
ページのトップへ