The Android Developer Challenge is back! Submit your idea before December 2.

パフォーマンスに関するヒント

このドキュメントでは主に、アプリの全体的なパフォーマンスの向上に役立つマイクロ最適化について説明します。ただし、こうした変更がパフォーマンスに大きな影響を及ぼすことはほとんどありません。適切なアルゴリズムとデータ構造を選択することは常に優先すべき事項ですが、これに関しては本ドキュメントの対象外です。本ドキュメントで紹介するヒントは、コーディングに関する一般的な手法としてご利用ください。これらの手法を習慣に取り入れることで、コードの効率性を全体的に高めることができます。

効率的なコードを作成するための基本的なルールとして、以下の 2 つが挙げられます。

  • 不要な処理を行わないこと。
  • メモリの割り当ては、できれば行わないようにすること。

Android アプリのマイクロ最適化において最も注意が必要な問題の 1 つは、アプリを各種ハードウェアで確実に実行できるようにすることです。さまざまなバージョンの VM があり、速度の異なる各種プロセッサ上で実行されます。一般に、「デバイス X はデバイス Y より F 倍高速 / 低速である」と断言したり、あるデバイスの結果を拡張して他のデバイスに適用したりすることはできません。特に、エミュレータでの測定では、デバイスのパフォーマンスに関する情報はほとんど得られません。また、JIT が搭載されているデバイスと搭載されていないデバイスでは大きな違いがあります。JIT が搭載されているデバイスにとって最適なコードが、JIT が搭載されていないデバイスにとっても最適なコードであるとは限りません。

さまざまなデバイスでアプリのパフォーマンスを適切に維持するには、あらゆるレベルでコードを効率化し、パフォーマンスを積極的に最適化する必要があります。

不要なオブジェクトを作成しない

オブジェクトの作成には常にコストがかかります。世代別ガベージ コレクタではスレッドごとに一時オブジェクト用の割り当てプールを使用して割り当てコストを抑えることができますが、メモリの割り当てを行う場合は必ずコストが生じます。

アプリ内で割り当てるオブジェクトが多くなると、ガベージ コレクションが定期的に実行されるため、そのたびにユーザー エクスペリエンスが一時的に低下します。Android 2.3 で導入された同時実行ガベージ コレクタは有効ですが、不要な処理は常に避ける必要があります。

このため、不要なオブジェクト インスタンスの作成を避けることが必要です。以下に、有用な例をいくつかご紹介します。

  • 文字列を返すメソッドがあり、その結果が必ず StringBuffer に付加されることがわかっている場合は、生存期間が短い一時オブジェクトを作成する代わりに、その付加処理を関数で直接行うように署名と実装を変更します。
  • 一連の入力データから文字列を抽出する場合は、コピーを作成する代わりに、元のデータの部分文字列を返すようにします。新しい String オブジェクトを作成することになりますが、このオブジェクトは char[] をデータと共有します(この方法にはトレードオフが存在し、元の入力データのごく一部のみを使用する場合でも、そのすべてをメモリ内にとにかく保持することになります)。

より基本的な考え方として、多次元配列を並列の 1 次元配列に分割する方法があります。

  • int の配列は Integer オブジェクトの配列よりもはるかに効率的ですが、一般論として、int の並列配列 2 つは (int,int) オブジェクトの配列 1 つよりもはるかに効率的です。プリミティブ型の任意の組み合わせについても同じことが言えます。
  • 複数の (Foo,Bar) オブジェクトを格納するコンテナを実装する必要がある場合は、一般に、Foo[]Bar[] の 2 つの並列配列のほうがカスタムの (Foo,Bar) オブジェクトの 1 つの配列よりもはるかに効率的です(もちろん、他のコードからアクセスする API を設計している場合は例外です。このような場合は通常、優れた API 設計の実現のために、速度については少し妥協することをおすすめします。ただし、独自の内部コードの場合は、できる限り効率性を高める必要があります)。

一般に、生存期間が短い一時オブジェクトはできれば作成しないでください。作成するオブジェクトが少なければ、ガベージ コレクションの実行頻度が低くなり、ユーザー エクスペリエンスを直接向上させることができます。

仮想より静的を優先する

オブジェクトのフィールドにアクセスする必要がない場合は、メソッドを静的にします。これにより、呼び出し速度が 15~20% 程度向上します。メソッドを呼び出してもオブジェクトの状態が変わらないことがメソッド シグネチャからわかることも、この方法の優れている点です。

定数に static final を使用する

クラスの先頭で次の宣言を行う場合について考えてみましょう。

    static int intVal = 42;
    static String strVal = "Hello, world!";
    

コンパイラは、クラスを初めて使用するときに実行される、<clinit> と呼ばれるクラス初期化メソッドを生成します。このメソッドは値 42 を intVal に格納し、strVal のクラスファイル文字列定数テーブルから参照を抽出します。これらの値を後で参照するときには、値へのアクセスにフィールド ルックアップが使用されます。

こうした状況は、「final」キーワードを使用することによって改善できます。

    static final int intVal = 42;
    static final String strVal = "Hello, world!";
    

定数が dex ファイルの静的フィールド初期化子に格納されるため、クラスの <clinit> メソッドは必要なくなります。intVal を参照するコードは整数値 42 を直接使用し、strVal へのアクセスではフィールド ルックアップではなく、相対的に低コストの「文字列定数」命令を使用します。

注: この最適化はプリミティブ型と String 定数にのみ適用されます。任意の参照型には適用されません。それでも、できる限り定数 static final を宣言することをおすすめします。

拡張版の for ループ構文を使用する

拡張版の for ループ(「for-each」ループと呼ばれることもあります)は、Iterable インターフェースを実装するコレクション、および配列で使用できます。コレクションでは、インターフェースで hasNext()next() を呼び出すためにイテレータが割り当てられます。ArrayList では、手書きのカウントループのほうが(JIT の有無に関係なく)約 3 倍高速ですが、他のコレクションでは、拡張版の for ループ構文は明示的にイテレータを使用する場合と完全に同等になります。

配列を反復処理する方法は他にもいくつかあります。

    static class Foo {
        int splat;
    }

    Foo[] array = ...

    public void zero() {
        int sum = 0;
        for (int i = 0; i < array.length; ++i) {
            sum += array[i].splat;
        }
    }

    public void one() {
        int sum = 0;
        Foo[] localArray = array;
        int len = localArray.length;

        for (int i = 0; i < len; ++i) {
            sum += localArray[i].splat;
        }
    }

    public void two() {
        int sum = 0;
        for (Foo a : array) {
            sum += a.splat;
        }
    }
    

JIT ではループの反復処理のたびに配列長を 1 回取得するコストを最適化できないため、zero() が最も低速になります。

one() はやや高速です。このメソッドでは、すべての値を取得してローカル変数に加算するため、ルックアップが行われません。パフォーマンス上のメリットは配列の長さだけです。

two() は、JIT が搭載されていないデバイスでは最も高速です。JIT が搭載されているデバイスでは one() と区別がつきません。このメソッドでは、Java プログラミング言語のバージョン 1.5 で導入された拡張版の for ループ構文が使用されています。

デフォルトで拡張版の for ループを使用する必要がありますが、パフォーマンスが重要な ArrayList の反復処理では、手書きのカウントループを使用することを検討してください。

ヒント: 「Effective Java」(Josh Bloch 著)の項目 46 もご覧ください。

プライベート内部クラスによるプライベート アクセスではなく、パッケージを検討する

次のクラス定義について考えてみましょう。

    public class Foo {
        private class Inner {
            void stuff() {
                Foo.this.doStuff(Foo.this.mValue);
            }
        }

        private int mValue;

        public void run() {
            Inner in = new Inner();
            mValue = 27;
            in.stuff();
        }

        private void doStuff(int value) {
            System.out.println("Value is " + value);
        }
    }

ここで重要なことは、外部クラスのプライベート メソッドとプライベート インスタンス フィールドに直接アクセスするプライベート内部クラス(Foo$Inner)を定義することです。これは正当な定義であり、コードは期待どおり「Value is 27」を出力します。

問題は、Java 言語では内部クラスから外部クラスのプライベート メンバーにアクセスできますが、FooFoo$Inner は別々のクラスであることから、VM で Foo$Inner から Foo のプライベート メンバーへの直接アクセスが不当とみなされることです。このギャップを埋めるために、コンパイラは以下の 2 つの合成メソッドを生成します。

    /*package*/ static int Foo.access$100(Foo foo) {
        return foo.mValue;
    }
    /*package*/ static void Foo.access$200(Foo foo, int value) {
        foo.doStuff(value);
    }

内部クラスのコードは、mValue フィールドにアクセスする必要があるときや、外部クラスの doStuff() メソッドを呼び出す必要があるときには必ず、これらの静的メソッドを呼び出します。つまり、上記のコードは実際のところ、アクセサ メソッドを介してメンバー フィールドにアクセスするケースということになります。前に、アクセサはフィールドへの直接アクセスよりも低速であることを説明しました。同じように、これは、特定の言語イディオムでは「目に見えない」パフォーマンスへの影響が生じる例を示しています。

パフォーマンス ホットスポットでこのようなコードを使用する場合、内部クラスからアクセスされるフィールドとメソッドを宣言し、プライベート アクセスではなくパッケージ アクセスを使用することで、オーバーヘッドを回避することができます。残念ながら、これは、同じパッケージ内の他のクラスからフィールドに直接アクセスできることを表します。そのため、公開 API ではこの方法は使用しないでください。

浮動小数点を使用しない

経験則として、Android デバイスでは、浮動小数点の処理には整数の約 2 倍の時間がかかります。

最近のハードウェアでは、floatdouble の間に処理速度に関する違いはありません。スペースについては、double のほうが 2 倍大きくなります。パソコンと同様に、スペースが問題にならないのであれば、float よりも double を使用することをおすすめします。

また、整数についても、プロセッサによっては、ハードウェア乗算はサポートしているがハードウェア除算はサポートしていないものもあります。このような場合、整数の除算および剰余演算はソフトウェアで行われます(ハッシュ テーブルの設計や各種の数値演算を行う場合は、このことについて検討する必要があります)。

ライブラリを理解し、利用する

独自のコードの展開よりもライブラリ コードを優先する通常の理由に加えて、システムはライブラリ メソッドの呼び出しをハンド コーディングされたアセンブラに自由に置き換えられることを覚えておいてください。このアセンブラは、JIT が同等の Java の代わりに生成できる最適なコードよりも効率的であることがあります。この典型的な例が String.indexOf() と関連 API です。Dalvik はこれらをインライン組み込み関数に置き換えます。同様に、JIT を搭載した Nexus One では、System.arraycopy() メソッドはハンド コーディングされたループよりも約 9 倍高速です。

ヒント: 「Effective Java」(Josh Bloch 著)の項目 47 もご覧ください。

ネイティブ メソッドは注意して使用する

ネイティブ コードを含むアプリを Android NDK を使用して開発する作業は、Java 言語でのプログラミングよりも必ずしも効率的であるとは言えません。一例を挙げると、Java には固有の移行関連コストが存在し、JIT ではその限界を越えて最適化することができません。ネイティブ リソース(ネイティブ ヒープ上のメモリ、ファイル ディスクリプタなど)を割り当てる場合、そのリソースのコレクションをタイミングよく配置することは非常に困難な場合があります。また、(JIT が搭載されていることに頼らずに)基盤となるアーキテクチャごとにコードをコンパイルする必要もあります。同じアーキテクチャと思われるものに対して複数のバージョンのコンパイルが必要になることさえあります。たとえば、G1 の ARM プロセッサ用にコンパイルされたネイティブ コードでは、Nexus One の ARM を最大限に活用することはできません。また、Nexus One の ARM 用にコンパイルされたコードは、G1 の ARM では実行できません。

ネイティブ コードは主に、Android に移植する既存のネイティブ コードベースがある場合に役立ちます。Java 言語で作成された Android アプリの「高速化」パーツには適していません。

ネイティブ コードを使用する必要がある場合は、JNI に関するおすすめの方法をご覧ください。

ヒント: 「Effective Java」(Josh Bloch 著)の項目 54 もご覧ください。

パフォーマンスに関する通説

JIT が搭載されていないデバイスでは、厳密な型が指定された変数を介してメソッドを呼び出すほうがインターフェースよりもやや効率的です(たとえば、HashMap mapMap map でメソッドを呼び出す場合、どちらもマップは HashMap ですが、前者のほうが低コストでした)。ただし、インターフェースでの呼び出し時間が 2 倍もかかったわけではなく、実際には 6% ほど長くなっただけです。さらに、JIT によってこの 2 つは実質的に区別できなくなっています。

JIT が搭載されていないデバイスでは、フィールドへのアクセスをキャッシュするほうがフィールドに繰り返しアクセスするよりも約 20% 高速です。JIT では、フィールドにアクセスするコストはローカル アクセスとほぼ同じです。そのため、コードの読みやすさが向上したと感じられなければ、最適化する価値はありません(これは、final、static、static final の各フィールドにも当てはまります)。

常に測定する

最適化を開始する前に、解決する必要がある問題があるかどうかを確認します。既存のパフォーマンスを正確に測定できるようにしてください。さもないと、代替手段のメリットを測定することができません。

Traceview はプロファイリングに役立つ場合もありますが、JIT が無効になることを考慮することが重要です。その結果、JIT による効率化が見込まれる場合のコードの時間が不正確になる可能性があります。Traceview のデータに基づいて変更を加えた後、Traceview なしで、実際に実行速度が改善したことを確認することが特に重要です。

アプリのプロファイリングとデバッグについて詳しくは、以下のドキュメントをご覧ください。