x86 上で Android* アプリを最適化するためのヒントとコツ

同カテゴリーの次の記事

GCC 5.0 での x86 向けの最適化新機能

この記事は、インテル® デベロッパー・ゾーンに掲載されている「Tips and Tricks to Optimize Android Apps on x86」の日本語参考訳です。


インテルは、開発者の皆さんがインテル® アーキテクチャー上で Android* アプリケーションを最適に実行できるように支援することに強い関心を持っています。Dalvik* Java*、V8 エンジン、および Bionic C の最適化、コードベースへの貢献、IA 向けの 32 ビットと 64 ビット・カーネルのリリースなど、コミュニティー・レベルでの取り組みに加えて、新しい Android* 開発支援ツールも開発しています。これらの取り組みの多くは、既存の x86 向け ARM* 変換レイヤーである libhoudini よりも優れたパフォーマンスを提供することを目的としています。

何はともあれ、最初にしなければならないことは、適切なツールを選択することです。Android* アプリを開発する主な方法は 3 つあります。

  1. Android* SDK API を利用して Java* で記述し、Dalvik* VM で実行する。
    注: Android L. による ART に関する記事が近日公開予定です。
    最新の SDK により大部分の違いはカバーされますが、高解像度の画面ではメモリー割り当てに注意を払う必要があります。インテル® HAXM を利用して Android* エミュレーターをスピードアップすると、テストを迅速に行うことができます (インテル® バーチャライゼーション・テクノロジーと XD を有効にする必要があります)。

  2. Web 向けに HTML5JavaScript で記述する。オープンソースの Android* 情報については、Android* on Intel Platforms サイト (英語) を参照してください。

  3. NDK を利用して作成する、または移植する (C++ コード)。この方法は、プロセッサー負荷の高い関数を含むアプリや既存の C++ コードがある場合によく採用されます。ネイティブ C++ コードは、実行前にバイナリーにコンパイルされ、マシン言語の解釈が不要なため、多くの場合より高速に実行できます (ただし、常にそうとは限りません)。

この記事では、NDK ベースのアプリについて説明します。NDK ベースのアプリは、C/C++ コードのみ、あるいはサードパーティー・ライブラリーやアセンブリー・コードを含んでいることがあります。

注: Android* 開発環境 (IDE) がまだインストールされていない場合、新しいインテル® INDE (インテル® Integrated Native Developer Experience) ツールスイートは、選択された Android* IDE をロードします。また、Android* アプリケーションの作成、コンパイル、トラブルシューティング、配布を支援するさまざまなインテル・ツールがダウンロードされインストールされます。インテル® INDE の登録とインストールについては https://software.intel.com/en-us/blogs/2014/06/16/getting-started-with-intel-inde を、Eclipse* IDE、NDK、SDK のセットアップとエミュレーターまたはインテル® アーキテクチャー・ベースのデバイスでの実行 (スピードアップする方法を含む) については https://software.intel.com/en-us/blogs/2014/06/20/installing-an-android-ide-with-eclipse-using-intel-inde を参照してください。

NDK による開発は次のステップで構成され、x86 アーキテクチャーで動作させるための変更は最小限で済みます。

  1. Android* プロジェクトと jni フォルダーを作成します。Application.mk を編集して、APP_ABI = all (ARM* と x86 を同じパッケージに含めてもファイルサイズに問題がない場合) または x86 に設定します。
    注: APP_ABI 設定は、浮動小数点処理に影響します (下記参照)。

  2. コードを記述します。ネイティブ (C++) コードは再利用し、 インライン・アセンブリー・コードまたは ARM* 固有コードは書き直します。javah で JNI/ネイティブ・コード・ヘッダー・ファイルを作成します。JNIEXPORT マクロと JNICALL マクロにより Windows* の C++ 標準規則と Java*/JNI を解釈するようにします。

  3. ライブラリーをコンパイル/ビルドします (.so ライブラリーが生成され、適切なプロジェクト・ディレクトリーに配置されます)。オプションを少し変更して “ndk-build APP_ABI = X86” を使用します (下記参照)。サードパーティー・ライブラリーがある場合はそれらも再コンパイルします。

  4. Java* からライブラリーを呼び出します。Java* でネイティブ (C++) 関数呼び出しを宣言し、System.loadlibrary() で共有ライブラリーをロードします。

  5. デバッグします。manifest で debuggable に設定し ndk-build を実行することで ndk-gdb debug を使用できます。adb ディレクトリーが PATH に追加され、1 つのターゲットのみ実行中であることを確認します。

基本的な “移植” に加えて、最適化を行うこともできます。

最適化のヒント:

  1. インテル® HAXM によりハードウェア支援によるエミュレーションを利用して、ソフトウェア・ベースの Android* エミュレーターをスピードアップします。インテル® HAXM を利用するには、インテル® バーチャライゼーション・テクノロジー (インテル® VT) と XD を有効にする必要があります。

  2. ファイルサイズの制限に応じて、APP_ABI = x86 (すべてのバイナリーを含む 1 つの apk を生成) または = armeabi armeabi-v7a x86 に設定します (x86 には armeabi-v7a-x86 とある程度同じハードウェア・ベースの浮動小数点演算が含まれます)。

  3. コンパイル時に、gcc “-malign-double” を使用します (これはメモリー・アライメントのためです – ヒント 9 も参照)。

  4. コンパイル時に、適切な CPU スレッドオプションを追加します。
    インテル® Atom™ プロセッサーのハイパースレッディング機能を有効するには、次のオプションを指定します: -mtune=atom -mssse3 -mfpmath=sse
    ハイパースレッディングを無効にするには (BYT、SLM、Merrifield)、次のオプションを指定します: -mtune=slm -msse4.2 -mfpmath=sse
    特定のCPU に限定するには -march= (http://gcc.gnu.org/onlinedocs/gcc/i386-and-x86-64-Options.html) を使用します (mtune はより多くのモデルで実行できますが、リストされている種類の最適化のみ行います)。
    -mavx は、インテル® Atom™ プロセッサーにはまだ効果がありません。

  5. リトル・エンディアン (NDK のデフォルト) を使用します。ARM* はビッグ・エンディアンとリトル・エンディアンの両方をサポートしていますが、インテル® Atom™ プロセッサーはリトル・エンディアンのみサポートしているため、gcc オプションを確認してください。

  6. gcc バージョン 4.8 を使用します。2 つのツールチェーンのパス (android-ndk\toolchains\arm-linux-androideabi-4.8 と x86 android-ndk\toolchains\x86-4.8) を確認します。

  7. 正しい JNIEXPORT メソッドのシグネチャーを使用し、ネイティブコードへのエントリーメソッドを設定します (Windows* 上でソースコードがコンパイルするように、ヘッダーファイルの関数のシグネチャーを一致させる必要があります)。
    JNIEXPORT void JNICALL Java_ClassName_MethodName

  8. コンパイル後、システムログをチェックし、実行時にターゲット・ネイティブ・ライブラリーがロードされることを確認します (ログの “added shared lib // ” という個所を確認します)。

  9. ロードエラーとネットワーク・パケットの問題を回避するため、明示的にメモリー・アライメントを強制します。ARM* は 24 バイトを使用し、64 ビット変数は 8 バイトでアライメントされている必要があります。一方、x86 は 16 バイトを使用するため、データ構造が 16 バイトでアライメントされるようにします。そして、そのデータ構造から XMM レジスターへ読み込む際は、アライメントされた移動 (MOVAPS、MOVNTA) を使用します。「アライメントされていないメモリーアクセスの影響の軽減」(英語) を参照してください。

  10. インテル® Atom™ プロセッサーには L3 キャッシュがないため、(ストリーミング・ストア命令 MOVNTPS、MOVNTQ を使用して) メインメモリーに直接データを書き込みます。これは、キャッシュ退避によるダーティー・ライトバックを回避し、帯域幅の使用も軽減します。

  11. L2 キャッシュの制限によるストールを回避します。特定の場合 (データのロードとストアが同じアドレスの場合、オペランドのサイズが同じ場合、汎用レジスターを使用する場合) を除き、インテル® Atom™ プロセッサーでロードを実行すると、キャッシュに書き込む間、数サイクルのストールが発生します。さらに、SSE オペランドのストア (xmm レジスターから) は、後続のロードにフォワードされません。
    そのため、フォワードする場合もしない場合も、xmm レジスターで合計処理を行い、レジスター内でデータを操作するようにします。例えば、mp3 デコーダーの場合、ループで合計を集計/計算してレジスターに格納し、その後すべてのレジスターの値を合計します。
    この場合、pSum 配列への 16 バイトのストアと後続の pSum からの 4 バイトのロードで、ブロックされたストアフォワードによりストールが発生します。これを回避するには、HADDPS 命令を使用するか、一連の加算とシャッフルを使用して、xmm レジスターで水平方向に合計を計算します (ただし、HADDPS 命令はインテル® Atom™ プロセッサーでは高速ですが、多くのインテル® Core™ プロセッサーでは遅くなるため注意が必要です)。また、16 ビットを超えるサンプルのクリッピングにインテル® SSE の min 命令と max 命令を利用できます。

  12. 一部の命令はレジスターのある部分のみをロードするため、残りの部分に設定されているコードにより問題が生じる可能性があります。そのため、使用前に、(MOVLPS、MOVHPS、PINSRW で) すべての XMM レジスターを 0 に設定します。

  13. SIMD 命令 (インテル® SSE と ARM* NEON*) の最適化に関する記事を読むと良いでしょう。(arm_neon.h の代わりに) こちらの記事から入手可能な NEONvsSSE_6.h ライブラリーの使用を検討してみてください。この記事では、定数を使用する場合のパフォーマンス・ペナルティー (ループ内で初期化せず、可能な場合は論理演算/比較演算に置換する) とシリアル実装の回避 (ベクトル化を使用する) についても説明しています。

  14. 除算と平方根演算には多くのサイクルが必要なため、テーブル・ルックアップ操作、逆数近似 (RCPPS 命令)、あるいはニュートン・ラプソン・シーケンスに置き換えることを検討します。

  15. 浮動小数点呼び出しに注意します。(倍精度は多くの場合ソフトウェア・ライブラリー・ルーチンを使用するため) 倍精度の代わりに単精度を使用します。単精度を使用したほうが、インテル® Atom™ プロセッサーではより高速に、より少ないメモリー帯域幅で処理できます。APP_ABI は、使用される浮動小数点演算がソフトウェア・ベース (armeabi) か、ハードウェア・ベース (X86、armeabi-v7a x86) かも設定します。x86 アルゴリズムをすべてハードウェア FPU で実行するのが良いとは限りません。例えば、2 で除算する場合、整数コードでは高速な右シフトに変換しますが、Android* 向けの最適化では逆数で乗算すべきです (y=x/2 の代わりに y=x*.5 を実行)。

  16. 小さな関数のオーバーヘッドを抑えます。パラメーターの引き渡し、新しいスタックフレームのセットアップ/古いスタックフレームの復元/呼び出し元のスタックフレームの保存、スタックへのアドレスの追加を含む領域、呼び出しの戻り値、リターンされた関数でインライン関数を使用します。『インテル® 64 アーキテクチャーおよび IA-32 アーキテクチャー最適化リファレンス・マニュアル』の 14 章と 15 章も参照してください。15 章は、こちらに参考訳があります。

コンパイラーの最適化に関する詳細は、最適化に関する注意事項を参照してください。

関連記事