プロファイル誘導最適化
rustc はプロファイル誘導最適化(PGO)をサポートしています。
この章では、PGO とは何か、そしてそのサポートが rustc でどのように
実装されているかを説明します。
プロファイル誘導最適化とは?
PGO の基本的な概念は、プログラムの典型的な実行に関するデータ (例: どの分岐を通る可能性が高いか)を収集し、そのデータを使用して インライン化、マシンコードの配置、レジスタ割り当てなどの最適化に 情報を与えることです。
プログラムの実行に関するデータを収集する方法はいくつかあります。
1 つは、プログラムをプロファイラー(perf など)の内部で実行する方法で、
もう 1 つは、インストルメント化されたバイナリ、つまりデータ収集機能が
組み込まれたバイナリを作成し、それを実行する方法です。
後者の方が通常、より正確なデータを提供します。
PGO は rustc でどのように実装されていますか?
rustc の現在の PGO 実装は完全に LLVM に依存しています。
LLVM は実際には PGO の複数の形式をサポートしています。
perfのような外部プロファイリングツールを使用してプログラムの実行に関する データを収集する、サンプリングベースの PGO。- コードカバレッジ基盤を使用してプロファイリング情報を収集する、 GCOV ベースのプロファイリング。
- コンパイラーフロントエンド(例: Clang)が、生成する LLVM IR に インストルメンテーション intrinsic を挿入する、フロントエンドベースの インストルメンテーション(ただし、1「注記」を参照)。
- LLVM が最適化パス中にインストルメンテーション intrinsic を自分で挿入する、 IR レベルのインストルメンテーション。
rustc がサポートしているのは最後のアプローチである IR レベルのインストルメンテーションのみです。主な理由は、
それがほぼ完全に LLVM で実装されており、Rust 側でほとんど
メンテナンスを必要としないためです。幸いなことに、これは最も現代的なアプローチでもあり、
最良の結果をもたらします。
つまり、ここで扱っているのはインストルメンテーションベースのアプローチです。すなわち、プロファイリングデータは、 最適化対象のプログラムを特別にインストルメント化したバージョンによって生成されます。 インストルメンテーションベースの PGO には、コンパイル時コンポーネントと 実行時コンポーネントの 2 つのコンポーネントがあり、それらがどのように相互作用するかを理解するには、 全体的なワークフローを理解する必要があります。
全体的なワークフロー
PGO で最適化されたプログラムの生成には、次の 4 つのステップが含まれます。
- インストルメンテーションを有効にしてプログラムをコンパイルする(例:
rustc -C profile-generate main.rs) - インストルメント化されたプログラムを実行する(例:
./main)。これによりdefault-<id>.profrawファイルが生成される - LLVM の
llvm-profdataツールを使用して、.profrawファイルを.profdataファイルに変換する。 - 今度はプロファイリングデータを使用して、プログラムを再度コンパイルする
(例:
rustc -C profile-use=merged.profdata main.rs)
コンパイル時の側面
上記のワークフローのどのステップにいるかに応じて、コンパイル時には 2 つの異なることが 起こり得ます。
インストルメンテーション付きのバイナリを作成する
前述のとおり、プロファイリングインストルメンテーションは LLVM によって追加されます。
rustc は、LLVM PassManager の作成時に適切なフラグを設定することで、
LLVM にそうするよう指示します。
// `PMBR` は `LLVMPassManagerBuilderRef` です
unwrap(PMBR)->EnablePGOInstrGen = true;
// インストルメント化されたバイナリには、`.profraw` ファイルのデフォルト出力パスが
// ハードコードされています:
unwrap(PMBR)->PGOInstrGen = PGOGenPath;
rustc はまた、LLVM のプロファイリングランタイムからの一部のシンボルが
適切なエクスポートレベルでマークすることによって削除されないようにする必要があります。
最適化がプロファイリングデータを使用するバイナリをコンパイルする
上記のワークフローの最後のステップでは、プログラムが再度コンパイルされ、
コンパイラーは収集されたプロファイリングデータを使用して最適化の判断を行います。
ここでも rustc は作業の大半を LLVM に任せており、基本的には LLVM PassManagerBuilder に
プロファイリングデータがどこにあるかを伝えるだけです。
unwrap(PMBR)->PGOInstrUse = PGOUsePath;
残りは LLVM が行います(例: 分岐の重みを設定する、関数に
cold や inlinehint をマークする、など)。
実行時の側面
インストルメンテーションベースのアプローチには常に実行時コンポーネントもあります。つまり、 インストルメント化されたプログラムができたら、そのプログラムを実行して プロファイリングデータを生成する必要があり、このプロファイリングデータを収集して永続化するには、 何らかの基盤が必要です。
LLVM の場合、これらの実行時コンポーネントは
compiler-rt に実装され、インストルメント化された
バイナリに静的リンクされます。
この rustc バージョンは library/profiler_builtins にあり、
基本的には compiler-rt の C コードを Rust クレートにまとめたものです。
profiler_builtins をビルドするには、rustc の bootstrap.toml で
profiler = true を設定する必要があります。
PGO のテスト
PGO ワークフローは複数のコンパイラー呼び出しにまたがるため、テストの大半は
run-make tests で行われます(関連するテストは名前に pgo を含みます)。
また、期待されるインストルメンテーションアーティファクトが LLVM IR に現れることを確認する
codegen test もあります。
追加情報
Clang のドキュメントには、LLVM における PGO に関する優れた概要が含まれています。
-
注記:
rustcは現在、実験的なオプション-C instrument-coverageによって、フロントエンドベースのカバレッジ インストルメンテーションをサポートしていますが、これらのカバレッジ結果を PGO に使用することは、 現時点では試みられていません。 ↩