Android ランタイム(ART)チームは、コンパイル済みコードやピーク時のメモリ回帰を損なうことなく、コンパイル時間を 18% 短縮しました。この改善は、メモリ使用量やコンパイル済みコードの品質を犠牲にすることなくコンパイル時間を短縮することを目的とした、2025 年の取り組みの一環です。
コンパイル時の速度を最適化することは、ART にとって非常に重要です。たとえば、ジャストインタイム(JIT)コンパイルを行うと、アプリケーションの効率とデバイス全体のパフォーマンスに直接影響します。コンパイルが高速化されると、最適化が開始されるまでの時間が短縮され、ユーザー エクスペリエンスがよりスムーズで応答性の高いものになります。さらに、JIT と事前(AOT)の両方で、コンパイル時間の短縮はコンパイル プロセス中のリソース消費の削減につながり、特にローエンド デバイスでバッテリー駆動時間とデバイスの温度にメリットをもたらします。
これらのコンパイル時の速度改善の一部は 2025 年 6 月の Android リリースでリリースされ、残りは年末の Android リリースでリリースされる予定です。また、Android バージョン 12 以降のすべてのユーザーは、メインライン アップデートを通じてこれらの改善を受けられます。
最適化コンパイラの最適化
コンパイラの最適化は常にトレードオフのゲームです。速度を無料で得ることはできません。何かを犠牲にする必要があります。Google は、コンパイラを高速化しつつ、メモリ回帰を導入せず、重要な点として、生成されるコードの品質を低下させないという、非常に明確で困難な目標を設定しました。コンパイラが高速になってもアプリの実行速度が遅くなったら、失敗です。
私たちが費やしてもよいと考えていた唯一のリソースは、これらの厳しい基準を満たす賢明なソリューションを深く掘り下げ、調査し、見つけるための開発時間でした。改善すべき点を見つけ、さまざまな問題に対する適切な解決策を見つけるために、Google がどのように取り組んでいるかについて詳しく見ていきましょう。
価値のある最適化の可能性を見つける
指標の最適化を開始する前に、その指標を測定できる必要があります。そうしないと、改善されたかどうかを判断できません。幸いなことに、変更前後の測定に使用するデバイスを同じものにする、デバイスでサーマル スロットリングが発生しないようにするなどの対策を講じれば、コンパイル時間の速度はかなり一定になります。さらに、コンパイラ統計などの決定論的な測定値も用意されており、内部で何が起こっているかを把握するのに役立ちます。
これらの改善のために犠牲にしたのは開発時間だったため、できるだけ早くイテレーションを実行したいと考えていました。そのため、Google は代表的なアプリ(ファーストパーティ製アプリ、サードパーティ製アプリ、Android オペレーティング システム自体を組み合わせたもの)をいくつか選び、ソリューションのプロトタイプを作成しました。その後、手動テストと自動テストの両方で広範囲にわたって、最終的な実装が価値のあるものであることを確認しました。
厳選した APK のセットを使用して、ローカルで手動コンパイルをトリガーし、コンパイルのプロファイルを取得して、pprof を使用して時間の費やし先を可視化します。
pprof のプロファイルのフレームグラフの例
pprof ツールは非常に強力で、データをスライス、フィルタ、並べ替えて、たとえば、どのコンパイラ フェーズまたはメソッドに最も時間がかかっているかを確認できます。pprof 自体の詳細については説明しません。バーが大きいほど、コンパイルに時間がかかったことを意味します。
これらのビューの 1 つは「ボトムアップ」ビューで、どのメソッドが最も時間を費やしているかを確認できます。下の図では、コンパイル時間の 1% 以上を占める Kill というメソッドが表示されています。その他の上位の方法についても、このブログ投稿の後半で説明します。
プロファイルのボトムアップ ビュー
最適化コンパイラには、グローバル値番号付け(GVN)と呼ばれるフェーズがあります。全体として何をするのかを気にする必要はありませんが、関連する部分は、フィルタに従って一部のノードを削除する `Kill` というメソッドがあることを知っておくことです。すべてのノードを反復処理して 1 つずつ確認する必要があるため、時間がかかります。その時点でアクティブなノードに関係なく、チェックが false になることが事前にわかっているケースがあることに気づきました。このような場合、反復処理を完全にスキップすることで、1.023% から約 0.3% に減らし、GVN のランタイムを約 15% 改善できます。
価値のある最適化の実装
時間の測定方法と、時間の費やされている場所の検出方法について説明しましたが、これはほんの始まりにすぎません。次のステップでは、コンパイルにかかる時間を最適化する方法について説明します。
通常、上記の `Kill` のようなケースでは、ノードの反復処理の方法を確認し、並列処理やアルゴリズムの改善などによって処理を高速化します。実際、最初はそうしようとしましたが、何もすることが見つからなかったときに「ちょっと待てよ…」と思い、解決策は(場合によっては)まったく繰り返さないことだと気づきました。このような最適化を行う場合、木を見て森を見ないということが起こりがちです。
その他のケースでは、次のようなさまざまな手法を使用しました。
- ヒューリスティクスを使用して、最適化が有益な結果を生成できないためスキップできるかどうかを判断する
- 計算されたデータをキャッシュに保存するために追加のデータ構造を使用する
- 現在のデータ構造を変更して速度を向上させる
- 場合によっては、結果を遅延計算してサイクルを回避する
- 適切な抽象化を使用する - 不要な機能はコードの速度を低下させる可能性があります
- 頻繁に使用されるポインタを多くの読み込みで追跡しないようにする
最適化を行う価値があるかどうかを判断するにはどうすればよいですか?
それが、この方法の優れた点です。コンパイルに時間がかかっている領域を特定し、開発時間を費やして改善を試みても、解決策が見つからないことがあります。何もすることがない、実装に時間がかかりすぎる、別の指標が大幅に悪化する、コードベースの複雑さが増すなど、さまざまな理由で実現しなかった最適化も数多くあります。このブログ投稿で紹介した最適化は、数多くの最適化のうちのほんの一部にすぎません。
同様の状況にある場合は、できるだけ少ない作業で指標をどの程度改善できるかを推定してみてください。つまり、次のようになります。
- すでに収集した指標または直感に基づいて見積もる
- クイック アンド ダーティ プロトタイプによる見積もり
- 解決策を実装します。
ソリューションの欠点を見積もることも忘れないでください。たとえば、追加のデータ構造に依存する場合、どの程度のメモリを使用するつもりですか?
詳細を学ぶ
それでは、実装した変更点を見ていきましょう。
FindReferenceInfoOf というメソッドを最適化する変更を実装しました。このメソッドは、エントリを見つけるためにベクトルの線形検索を行っていました。そのデータ構造を更新して、命令の ID でインデックス登録できるようにしました。これにより、FindReferenceInfoOf は O(n) ではなく O(1) になります。また、サイズ変更を避けるために、ベクトルを事前に割り当てました。ベクトルに挿入したエントリ数をカウントする追加のフィールドを追加する必要があったため、メモリをわずかに増やしましたが、ピーク時のメモリは増えなかったため、小さな犠牲ですみました。これにより、LoadStoreAnalysis フェーズが 34 ~ 66% 高速化され、コンパイル時間が約 0.5 ~ 1.8% 改善されました。
HashSet のカスタム実装があり、複数の場所で使用しています。このデータ構造の作成にかなりの時間がかかっていましたが、その理由がわかりました。このデータ構造は、非常に大きな HashSet を使用する少数の場所でのみ使用され、そのために最適化されるように調整されました。しかし、最近では、エントリが少なく、有効期間が短い状態で逆方向に使われることが多くなっています。つまり、この巨大な HashSet を作成しても、数個のエントリにしか使用されず、その後破棄されるため、サイクルが無駄になっていました。この変更により、コンパイル時間が約 1.3 ~ 2% 改善されました。また、以前ほど大きなデータ構造を使用しなくなったため、メモリ使用量が約 0.5 ~ 1% 減少しました。
ラムダに 参照でデータ構造を渡すことで、コピーを回避し、コンパイル時間を約 0.5 ~ 1% 改善しました。これは元のレビューで見落とされ、何年もコードベースに存在していました。これらのメソッドが多くのデータ構造を作成して破棄していることに気づいたのは、pprof のプロファイルを確認したことがきっかけでした。これにより、調査と最適化を行うことができました。
コンパイルされた出力を書き込むフェーズを高速化するため、計算された値をキャッシュに保存しました。これにより、コンパイル時間の合計が約 1.3 ~ 2.8% 改善されました。残念ながら、余分な簿記処理が多すぎたため、自動テストでメモリの回帰が検出されました。その後、同じコードを再度確認し、メモリの回帰に対処するだけでなく、コンパイル時間をさらに約 0.5 ~ 1.8% 改善する新しいバージョンを実装しました。この 2 回目の変更では、2 つのデータ構造の 1 つを削除するために、このフェーズの動作方法をリファクタリングして再考する必要がありました。
最適化コンパイラには、パフォーマンスを向上させるために関数呼び出しをインライン化するフェーズがあります。インライン化するメソッドを選択するために、計算を行う前にヒューリスティックを使用し、作業後、インライン化を確定する直前に最終チェックを行います。これらのいずれかでインライン化する価値がないと判断された場合(新しい命令が多すぎるなど)、メソッド呼び出しはインライン化されません。
時間のかかる計算を行う前にインライン化が成功するかどうかを推定するため、「最終チェック」カテゴリの 2 つのチェックを「ヒューリスティック」カテゴリに移動しました。これは推定値であるため完全ではありませんが、新しいヒューリスティックは、パフォーマンスに影響を与えることなく、以前にインライン化されたものの 99.9% をカバーすることが確認されています。これらの新しいヒューリスティックの 1 つは必要な DEX レジスタ(約 0.2 ~ 1.3% の改善)に関するもので、もう 1 つは命令数(約 2% の改善)に関するものでした。
BitVector のカスタム実装があり、複数の場所で使用しています。特定の固定サイズのビットベクトルに対して、サイズ変更可能な BitVector クラスをよりシンプルな BitVectorView に置き換えました。これにより、間接参照とランタイム範囲チェックが一部排除され、ビットベクター オブジェクトの構築が高速化されます。
さらに、BitVectorView クラスは、基盤となるストレージ型でテンプレート化されました(以前の BitVector のように常に uint32_t を使用するのではなく)。これにより、Union() などの一部のオペレーションで、64 ビット プラットフォームで 2 倍のビットを同時に処理できます。Android OS のコンパイル時に、影響を受ける関数のサンプルが合計で 1% 以上削減されました。これは、いくつかの変更 [1、2、3、4、5、6] にわたって行われました。
すべての最適化について詳しく説明すると、一日中かかってしまいます。その他の最適化にご興味がある場合は、実装したその他の変更点をご覧ください。
- 簿記を追加して、コンパイル時間を約 0.6 ~ 1.6% 改善します。
- 可能であれば、サイクルを回避するためにデータを遅延計算します。
- 使用されない場合は事前計算の作業をスキップするようにコードをリファクタリングします。
- アロケータを他の場所から簡単に取得できる場合は、依存するロードチェーンの一部を回避します。
- 不要な作業を避けるためのチェックを追加する別のケース。
- レジスタ アロケータでレジスタ タイプ(コア/FP)の頻繁な分岐を回避。
- コンパイル時に一部の配列が初期化されていることを確認します。clang に依存しないでください。
- ループをクリーンアップします。clang がより最適化できる範囲ループを使用します。これは、ループの副作用によりコンテナの内部ポインタを再読み込みする必要がないためです。各入力に対してインライン化された `InputAt(.)` を介して、ループ内で仮想関数 `HInstruction::GetInputRecords()` を呼び出すことを回避します。
- コンパイラ最適化を利用して、ビジター パターンの Accept() 関数を回避します。
まとめ
ART のコンパイル時間の短縮に注力した結果、大幅な改善が実現し、Android の動作がよりスムーズかつ効率的になり、バッテリーの駆動時間とデバイスの温度の改善にも貢献しています。最適化を念入りに特定して実装することで、メモリ使用量やコード品質を損なうことなく、コンパイル時間を大幅に短縮できることを実証しました。
この過程では、pprof などのツールを使用したプロファイリング、反復処理への意欲、場合によっては成果の少ない方法を放棄することも必要でした。ART チームの共同作業により、コンパイル時間が大幅に短縮されただけでなく、将来の進歩に向けた基盤も築かれました。
これらの改善はすべて、2025 年末の Android アップデートで利用可能になるほか、Android 12 以降では Mainline アップデートを通じて利用可能になります。この最適化プロセスの詳細な分析が、コンパイラ エンジニアリングの複雑さとメリットに関する貴重な知見を提供できれば幸いです。
-
プロダクト ニュースGoogle Play では、サブスクリプション プラットフォームを継続的に拡大し、成長の促進、新しいビジネスモデルへの適応、ユーザーのニーズへの対応をサポートしています。
Sheenam Mittal • 所要時間: 4 分 -
プロダクト ニュース昨年、Android Studio はあらゆる AI モデルに対応しました。本日、Google はコーディング エージェントの選択をサポートすることで、次のステップに進みます。
Matthew Warner • 所要時間: 3 分 -
プロダクト ニュースGooglebook は、共有 Android 基盤上に構築された新しいカテゴリのノートパソコンです。HP、Dell、Lenovo、Acer、Asus などのパートナーの高性能ハードウェアは、モバイルの利便性とデスクトップのパワーを兼ね備えています。
Fahd Imtiaz, Loryn Hairston • 所要時間: 4 分
Android 開発に関する最新の分析情報を毎週メールでお届けします。