このドキュメントでは主に、アプリの全体的なパフォーマンスの向上に役立つ各種のマイクロ最適化について説明します。ただし、いずれもパフォーマンスに劇的な影響を及ぼすような変更ではありません。パフォーマンスを改善するうえで最優先すべきなのは、適切なアルゴリズムとデータ構造を選択することですが、これに関しては本ドキュメントの対象外です。本ドキュメントで紹介するヒントは、コーディングに関する一般的な手法としてご利用ください。この手法を習慣に取り入れることで、コードの効率性を全体的に高めることができます。
効率的なコードを作成する際の基本ルールとしては、次の 2 つが挙げられます。
- 不要な処理を行わないこと。
- メモリの割り当ては、できれば行わないようにすること。
Android アプリのマイクロ最適化において、まず考慮すべき点は、アプリをさまざまなタイプのハードウェア上で確実に実行できるようにすることです。さまざまなバージョンの VM があり、速度の異なる各種プロセッサ上で実行されます。一般に、「デバイス X はデバイス Y より F 倍高速 / 低速である」と断言したり、あるデバイスの結果を拡張して他のデバイスに適用したりすることはできません。特に、エミュレータでの測定では、デバイスのパフォーマンスに関する情報はほとんど得られません。また、JIT を搭載しているデバイスと搭載していないデバイスでは大きな違いがあります。JIT を搭載しているデバイスにとって最適なコードであっても、JIT を搭載していないデバイスにとって最適なコードとは限りません。
さまざまなデバイスでアプリのパフォーマンスを適切に維持するには、あらゆるレベルでコードを効率化し、パフォーマンスを積極的に最適化する必要があります。
不要なオブジェクトを作成しない
オブジェクトの作成には常にコストがかかります。世代別ガベージ コレクタではスレッドごとに一時オブジェクト用の割り当てプールを使用して割り当てコストを抑えることができますが、メモリの割り当てを行う場合は必ずコストが生じます。
アプリ内で割り当てるオブジェクトが多くなると、ガベージ コレクションが定期的に実行されるため、そのたびにユーザー エクスペリエンスが一時的に低下します。Android 2.3 で導入された同時実行ガベージ コレクタは有効ですが、不要な処理は常に避ける必要があります。
このため、不要なオブジェクト インスタンスの作成を避けることが必要です。有用な例を以下に示します。
- 文字列を返すメソッドがあり、その結果が必ず
StringBuffer
に付加されることがわかっている場合は、短期的な一時オブジェクトを作成するのではなく、その付加処理を関数で直接行うようにシグネチャと実装を変更します。 - 入力データセットから文字列を抽出する場合は、コピーを作成するのではなく、元のデータの部分文字列を返すようにします。新しい
String
オブジェクトを作成しますが、このオブジェクトはchar[]
をデータと共有します(この方法にはトレードオフが存在し、元の入力データのごく一部だけを使用する場合でも、常に元のデータをすべてメモリ内に保持することになります)。
もっと根本的な考え方として、多次元配列を並列の 1 次元配列に分割する方法があります。
Integer
オブジェクトの配列よりもint
の配列の方がはるかに効率的です。同様に一般論として、(int,int)
オブジェクトの配列 1 つよりも int の並列配列 2 つの方がはるかに効率的です。プリミティブ型の任意の組み合わせについて、同じことが言えます。- タプル型の
(Foo,Bar)
オブジェクトを格納するコンテナを実装する必要がある場合、一般に、カスタム(Foo,Bar)
オブジェクトの 1 つの配列よりもFoo[]
とBar[]
の 2 つの並列配列の方がはるかに効率的です(もちろん、他のコードからアクセスする 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; } }
zero()
が最も低速です。JIT を搭載していても、ループの反復処理のたびに配列長を 1 回取得するコストを最適化できません。
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 言語では、内部クラスから外部クラスのプライベート メンバーにアクセスすることが認められていますが、Foo
と Foo$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 倍の時間がかかります。
最近のハードウェアの場合、float
と double
の間に処理速度に違いはありません。スペースについては、double
の方が 2 倍大きくなります。したがって、パソコンと同様に、スペースが問題にならないのであれば、float
よりも double
を使用することをおすすめします。
また、整数に関しても、プロセッサによっては、ハードウェア乗算はサポートしていてもハードウェア除算はサポートしていない場合があります。このような場合、整数の除算および剰余演算はソフトウェアで行われます(ハッシュ テーブルの設計や各種の数値演算を行う場合は、このことについて検討する必要があります)。
ライブラリを理解し、利用する
一般的に、独自コードを導入するよりもライブラリ コードを使用することをおすすめしますが、必要に応じて、Android システムは、ライブラリ メソッドの呼び出しをハンド コーディング アセンブラに自由に置き換えることができます。このようなアセンブラは、同等の Java の代わりに JIT が生成できる最高のコードよりも優れている場合もあります。この典型的な例が String.indexOf()
および関連 API です。Dalvik はこのようなコードをインライン イントリンシック関数に置き換えます。同様に、JIT を搭載した Nexus One 上で、System.arraycopy()
メソッドは、ハンド コーディング ループよりも約 9 倍高速になります。
ヒント: 『Effective Java』(Josh Bloch 著)の項目 46 もご覧ください。
ネイティブ メソッドは注意して使用する
ネイティブ コードを含むアプリを 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 map
と Map map
に対してメソッドを呼び出す場合、どちらもマップは HashMap
ですが、前者の方が低コストでした)。ただし、インターフェース経由の場合に呼び出し時間が 2 倍もかかったわけではなく、実際には 6% ほど長くなっただけでした。また、JIT により、2 つの呼び出し方法の差はほとんどなくなっています。
JIT が搭載されていないデバイスでは、フィールドへのアクセスをキャッシュするほうがフィールドに繰り返しアクセスするよりも約 20% 高速です。JIT を搭載している場合、フィールド アクセスのコストは、ローカル アクセスとほぼ同じになります。そのため、コードの読みやすさが向上するのでない限り、この最適化は価値を持ちません(これは、final、static、static final の各フィールドにも当てはまります)。
常に測定する
最適化を開始する前に、解決する必要がある問題があるかどうかを確認します。既存のパフォーマンスを正確に測定できるようにしてください。そうでなければ、代替手段のメリットを測定することもできません。
なお、Traceview はプロファイリングに役立つ場合もありますが、現在のところ、JIT が無効化されてしまう点に注意が必要です。JIT による効率化が見込まれる部分のコードの時間が不正確になる可能性があります。Traceview のデータに基づいて変更を加えた後、Traceview なしで、実際に実行速度が改善したことを確認することが特に重要です。
アプリのプロファイリングとデバッグについては、以下のドキュメントをご覧ください。