Android 8.0 ART の改善点

Android ランタイム(ART)は、Android 8.0 リリースで大幅に改善されました。以下では、デバイス メーカーが ART で利用できる拡張機能の概要を示します。

同時圧縮ガベージ コレクタ

Google I/O でお知らせしたように、Android 8.0 の ART では、新しい同時圧縮ガベージ コレクタ(GC)が提供されます。このコレクタは、GC の実行時(毎回)とアプリの実行中にヒープを圧縮し、スレッドルートを処理するために 1 回だけ短い一時停止を行います。そのメリットは次のとおりです。

  • GC は常にヒープを圧縮します。Android 7.0 と比較して、ヒープサイズは平均で 32% 小さくなります。
  • 圧縮により、スレッド ローカルなバンプポインタ オブジェクトの割り当てが可能になります。割り当ては Android 7.0 より 70% 高速です。
  • Android 7.0 の GC と比較して、H2 ベンチマークの一時停止時間が 85% 短くなります。
  • 一時停止時間がヒープサイズに比例することはなくなりました。アプリでは、ジャンクを気にせずに大きなヒープを使用できます。
  • GC 実装の詳細 - 読み取りバリア:
    • 読み取りバリアは、オブジェクト フィールドの読み取りごとに行われる小さな処理です。
    • コンパイラで最適化されますが、ユースケースによっては低速になることがあります。

ループ最適化

Android 8.0 リリースの ART では、さまざまなループ最適化が使用されます。

  • 境界チェックの除去
    • 静的: コンパイル時に範囲が境界内にあることが証明されます
    • 動的: 実行時テストによりループが境界内にとどまることが確認されます(そうならない場合は最適化解除されます)
  • 帰納変数の除去
    • 使用されなくなった帰納変数の除去
    • 閉形式の式によってループ後にのみ使用される帰納変数の置き換え
  • ループ本体の内部の使用されなくなったコードの除去、使用されなくなったループ全体の除去
  • 強度の削減
  • ループ変換: 反転、交換、分割、展開、ユニモジュラなど
  • SIMD 化(ベクトル化とも呼びます)

ループ オプティマイザは、ART コンパイラの固有の最適化パス内に存在します。ほとんどのループ最適化は、他のケースの最適化、簡素化と似ています。多くの CFG ユーティリティ(nodes.h を参照)は、CFG の書き換えではなく作成に焦点を合わせているため、CFG を通常より複雑な方法で書き換える最適化では困難が生じます。

クラス階層分析

Android 8.0 の ART はクラス階層分析(CHA)を使用します。これは、階層構造の分析により生成された情報に基づいて、仮想呼び出しを直接呼び出しに非仮想化するコンパイラ最適化です。仮想呼び出しは、vtable ルックアップに実装されており複数の依存読み込みを行うため、高コストです。また、仮想呼び出しはインライン化できません。

関連する拡張機能の概要を次に示します。

  • 単一実装の動的メソッドのステータス更新 - クラスのリンク時間の終わりに vtable にデータが入力されると、ART はスーパークラスの vtable に対してエントリごとの比較を行います。
  • コンパイラ最適化 - コンパイラはメソッドの単一実装情報を利用します。メソッド A.foo に単一実装フラグが設定されている場合、コンパイラは仮想呼び出しを直接呼び出しに非仮想化し、結果としてさらに直接呼び出しのインライン化を試みます。
  • コンパイル済みコードの無効化 - また、クラスのリンク時間の終わりに単一実装の情報が更新されると、以前は単一実装だったメソッド A.foo のステータスが無効化された場合、メソッド A.foo が単一実装であるという前提に依存するすべてのコンパイル済みコードで、それらのコンパイル済みコードを無効化する必要があります。
  • 最適化解除 - スタック上にあるコンパイル済みライブコードの場合、正確性を保証するために、最適化解除が起動されて、無効化されたコンパイル済みコードが強制的にインタープリタ モードに変換されます。同期と非同期の最適化解除を混合した新しい最適化解除メカニズムが使用されます。

.oat ファイル内のインライン キャッシュ

ART は、インライン キャッシュを使用して、十分なデータが存在するコールサイトを最適化するようになりました。インライン キャッシュ機能により、追加のランタイム情報がプロファイルに記録され、その情報を使用して動的最適化が事前コンパイルに追加されます。

Dexlayout

Android 8.0 で導入された Dexlayout は、dex ファイルを解析し、プロファイルに従って並べ替えるためのライブラリです。Dexlayout の狙いは、ランタイムのプロファイリング情報を使用して、デバイスでアイドル状態のメンテナンス コンパイルを行う際に dex ファイルのセクションを並べ替えることです。まとめてアクセスされることが多い dex ファイルの構成要素をグループ化することで、プログラムの局所性を改善してメモリアクセス パターンを効率化し、RAM の節約と起動時間の短縮を実現できます。

現在、プロファイル情報はアプリの実行後にのみ利用可能になるため、dexlayout は、アイドル状態のメンテナンス中に行われる dex2oat のオンデバイス コンパイルに統合されています。

Dex キャッシュの削除

Android 7.0 まで、DexCache オブジェクトは DexFile の特定の要素の数に比例する次の 4 つの大きな配列を持っていました。

  • 文字列(DexFile::StringId ごとに参照が 1 つ)
  • 型(DexFile::TypeId ごとに参照が 1 つ)
  • メソッド(DexFile::MethodId ごとにネイティブ ポインタが 1 つ)
  • フィールド(DexFile::FieldId ごとにネイティブ ポインタが 1 つ)

これらの配列は、以前解決されたオブジェクトの高速検索に使用されていました。Android 8.0 では、メソッド配列以外のすべての配列が削除されました。

インタープリタのパフォーマンス

Android 7.0 リリースで mterp が導入されたことにより、インタープリタのパフォーマンスは大幅に向上しました。mterp は、アセンブリ言語で記述されたコアなフェッチ / デコード / 解釈メカニズムを備えたインタープリタです。mterp は高速の Dalvik インタープリタを原型として開発されており、arm、arm64、x86、x86_64、mips、mips64 をサポートします。計算コードの処理速度については、ART の mterp は Dalvik の高速インタープリタにほぼ匹敵します。ただし、次の状況では、大幅に(ときには急激に)低速になる可能性があります。

  1. 呼び出しパフォーマンス。
  2. 文字列操作と、Dalvik の組み込み関数として認識されるメソッドの頻繁な使用。
  3. スタックメモリの使用量の増加。

Android 8.0 では、これらの問題は解決済みです。

インライン化の拡張

Android 6.0 以降の ART は、同じ dex ファイル内では任意の呼び出しをインライン化できますが、異なる dex ファイルからインライン化できるのはリーフメソッドのみでした。この制限には 2 つの理由がありました。

  1. 別の dex ファイルからのインライン化では、呼び出し元の dex キャッシュを再使用するだけで済む同じ dex ファイル内のインライン化とは異なり、他の dex ファイルの dex キャッシュを使用する必要があります。dex キャッシュは、静的呼び出し、文字列の読み込み、クラスの読み込みなど、いくつかの命令用としてコンパイル済みコードに必要です。
  2. スタックマップは、現在の dex ファイル内のメソッド インデックスのみをエンコードしています。

Android 8.0 では、これらの制限事項に対処するため、次の改善が行われました。

  1. コンパイル済みコードから dex キャッシュへのアクセスが削除されました(「Dex キャッシュの削除」セクションもご覧ください)。
  2. スタックマップのエンコードが拡張されました。

同期処理の改善

ART チームは、MonitorEnter/MonitorExit コードパスを調整し、ARMv8 での従来のメモリバリアへの依存を少なくして、可能な限りそれらを新しい(acquire/release)命令に置き換えました。

ネイティブ メソッドの高速化

@FastNative アノテーションと @CriticalNative アノテーションの使用により、Java Native Interface(JNI)のネイティブ呼び出しが高速になりました。これらの組み込み ART ランタイムの最適化では、JNI の遷移が高速化され、サポートが終了した !bang JNI 表記が置き換えられます。上記のアノテーションは非ネイティブ メソッドには影響せず、bootclasspath 上のプラットフォーム Java 言語コードに対してのみ使用できます(Play ストアではアップデートされません)。

@FastNative アノテーションは非静的メソッドをサポートします。メソッドがパラメータまたは戻り値として jobject にアクセスする場合は、このアノテーションを使用してください。

@CriticalNative アノテーションを使用するとネイティブ メソッドの実行をさらに高速化できますが、次の制限があります。

  • メソッドは静的でなければなりません。パラメータ、戻り値、暗黙的な this 用のオブジェクトはありません。
  • ネイティブ メソッドに渡されるのはプリミティブ型のみです。
  • ネイティブ メソッドは、その関数定義で JNIEnv パラメータと jclass パラメータを使用しません。
  • 動的 JNI リンクに依存せず、RegisterNatives でメソッドを登録する必要があります。

@FastNative はネイティブ メソッドのパフォーマンスを最大で 3 倍高速化し、@CriticalNative は最大で 5 倍高速化します。Nexus 6P デバイスで測定された JNI の遷移の例を次に示します。

Java Native Interface(JNI)の呼び出し 実行時間(ナノ秒単位)
通常の JNI 115
!bang JNI 60
@FastNative 35
@CriticalNative 25