プロファイルに基づく最適化

プロファイルに基づく最適化(PGO)は、よく知られたコンパイラ最適化手法です。PGO では、プログラムの実行からのランタイム プロファイルがコンパイラで使用されて、インライン化とコード レイアウトに関する最適な選択が行われます。これにより、パフォーマンスが向上し、コードのサイズが削減されます。

PGO をアプリやライブラリにデプロイする手順は次のとおりです。 1. 典型的なワークロードを特定する。 2. プロファイルを収集する。 3. リリースビルドでプロファイルを使用する。

ステップ 1: 典型的なワークロードを特定する

まず、アプリの典型的なベンチマークまたはワークロードを特定します。これは重要なステップで、ワークロードから収集したプロファイルによってコード内のホット リージョンとコールド リージョンを特定します。プロファイルを使用する際、コンパイラはホット リージョンで積極的な最適化とインライン化を行います。また、コンパイラは、パフォーマンスと引き換えにコールド リージョンのコードのサイズの削減を選ぶこともできます。

一般に、適切なワークロードを特定することは、パフォーマンスを管理するうえでも役に立ちます。

ステップ 2: プロファイルを収集する

プロファイルの収集は、インストルメンテーションによるネイティブ コードのビルド、デバイスでのインストルメント済みアプリの実行とプロファイルの生成、ホストでのプロファイルの統合 / 後処理の 3 つのステップで構成されます。

インストルメント済みビルドを作成する

プロファイルを収集するには、アプリのインストルメント済みビルドでステップ 1 で特定したワークロードを実行します。インストルメント済みビルドを生成するには、コンパイラ フラグとリンカーフラグに -fprofile-generate を追加します。このフラグは、デフォルト ビルドの際は不要なため、別個のビルド変数で制御する必要があります。

プロファイルを生成する

次に、デバイスでインストルメント済みアプリを実行し、プロファイルを生成します。 インストルメント済みバイナリの実行時にプロファイルがメモリに収集され、終了時にファイルに書き込まれます。ただし、atexit で登録された関数は Android アプリでは呼び出されません。アプリは単に強制終了されます。

アプリ / ワークロードは、プロファイル ファイルのパスを設定して、プロファイルの書き込みを明示的にトリガーするための追加作業を行う必要があります。

  • プロファイル ファイルのパスを設定するには、__llvm_profile_set_filename(PROFILE_DIR "/default-%m.profraw を呼び出します。%m は、複数の共有ライブラリがある場合に役に立ちます。%m` は、そのライブラリの一意のモジュール署名に展開されるため、ライブラリごとに別々のプロファイルが生成されます。その他の役に立つパターン指定子については、こちらをご覧ください。PROFILE_DIR は、アプリから書き込み可能なディレクトリです。実行時にこのディレクトリを検出する方法については、デモをご覧ください。
  • プロファイルの書き込みを明示的にトリガーするには、__llvm_profile_write_file 関数を呼び出します。
extern "C" {
extern int __llvm_profile_set_filename(const char*);
extern int __llvm_profile_write_file(void);
}

#define PROFILE_DIR "<location-writable-from-app>"
void workload() {
  // ...
  // run workload
  // ...

  // set path and write profiles after workload execution
  __llvm_profile_set_filename(PROFILE_DIR "/default-%m.profraw");
  __llvm_profile_write_file();
  return;
}

注意: ワークロードがスタンドアロン バイナリであれば、プロファイル ファイルの生成はさらに簡単になります。バイナリを実行する前に LLVM_PROFILE_FILE 環境変数を %t/default-%m.profraw に設定するだけです。

プロファイルを後処理する

プロファイルのファイル形式は .profraw です。まず、adb pull を使用してデバイスからこのファイルを取得する必要があります。取得後、NDK の llvm-profdata ユーティリティを使用して .profraw から .profdata に変換します。これで、コンパイラに渡すことができるようになります。

$NDK/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-profdata \
    merge --output=pgo_profile.profdata \
    <list-of-profraw-files>

プロファイルのファイル形式のバージョン不一致を回避するには、同じ NDK リリースの llvm-profdataclang を使用します。

ステップ 3: プロファイルを使用してアプリをビルドする

コンパイラとリンカーに -fprofile-use=<>.profdata を渡して、アプリのリリースビルド時に前の手順で作成したプロファイルを使用します。このプロファイルは、コードが発展した場合でも使用できます。Clang コンパイラは、ソースとプロファイルの不一致がわずかであれば許容できます。

注意: 一般に、ほとんどのライブラリの場合、プロファイルはアーキテクチャ全体で共通です。たとえば、ライブラリの arm64 ビルドから生成されたプロファイルは、すべてのアーキテクチャで使用できます。注意点として、ライブラリにアーキテクチャ固有のコードパスがある場合は(arm と x86 または 32 ビットと 64 ビット)、そうした設定ごとに個別のプロファイルを使用する必要があります。

すべてを組み合わせる

https://github.com/DanAlbert/ndk-samples/tree/pgo/pgo に、PGO を使用する方法についてのアプリのサンプルデモがあります。この記事で省略した詳しい情報をご覧いただけます。

  • CMake ビルドのルールには、インストルメンテーションを使ってネイティブ コードをビルドする CMake 変数のセットアップ方法が紹介されています。ビルド変数を設定しない場合、ネイティブ コードは、以前生成された PGO プロファイルを使用して最適化されます。
  • インストルメント済みビルドでは、pgodemo.cpp によって、プロファイルがワークロードの実行であると書き込まれます。
  • プロファイルの書き込み可能な場所は、applicationContext.cacheDir.toString() を使用して実行時に MainActivity.kt で取得されます。
  • adb root を必要とすることなくデバイスからプロファイルを引き出すには、こちらadb レシピを使用します。