Android アプリケーションは限りある計算能力とデータ記憶域、より小さい画面、および制約のあるバッテリ寿命のモバイルデバイス上で実行されます。この理由から、効率的であるべきです。アプリがすでに十分 "速く動いている" ように見えても、バッテリの寿命は最適化しておいたほうがいいでしょう。バッテリの寿命はユーザにとって重要であり、Android のバッテリの使用可能な量が減ってしまうと、アプリがバッテリ食いの張本人であることがユーザに知られてしまうことになります。
このドキュメントは主に細かい部分での最適化を取り上げていますが、ソフトウェアの成功を左右するものではないという点に注意してください。適切なアルゴリズムやデータ構造が常に優先されなければなりませんが、それについてはこのドキュメントの範囲外とさせてもらいます。
まえがき
効率的なコードを書くには、以下のふたつの基本ルールがあります。
- やる必要のない処理は行わない。
- 避けられるのであればメモリをアロケートしない。
よく考えて最適化する
このドキュメントは Android に特化した細かい最適化について述べていますが、みなさんが取り組んできた、最適化に必要なコードの真のあるべき姿を実現するための分析方法はすでに知っていて、行ったさまざまな修正に対する効果 ( 良くも悪くも ) を評価する方法を、みなさんはすでに持ち合わせているということを前提としています。作業に注ぎ込む時間が限られていれば、それを賢く使うことが大切であることも良くお分かりだと思います。
( 効果的なベンチマークについての詳しい分析と作成については あとがき を参照してください。)
このドキュメントでは、データ構造やアルゴリズムに関してベストな判断が行われていて、API の選別の結果も将来的なパフォーマンスを考慮していることを前提としています。正しいデータ構造とアルゴリズムを使うことにより、ここでアドバイスしているよりもはるかに効果があり、パフォーマンスを考慮したAPI を選別しておけば、将来の実装に切替えやすくなります ( これはアプリケーションコードよりもライブラリコードに対し、より重要となります ) 。
( この種のアドバイスを求める場合は、Josh Bloch の Effective Java、アイテム 47 を参照してください )
Android アプリで細かな最適化を行う際、アプリは多様なデバイスで動作するということが保証されているということがもっとも厄介な課題のひとつとして直面します。異なる速度で実行する異なるプロセッサで実行する VM の異なるバージョンのことです。"デバイス X はデバイス Y より F 倍速いとか遅い" とか言えたり、あるデバイスから他になったときの速度を推測することが簡単にできるかというと、普通はそういうことにはなりません。特に、エミュレータでの測定値がどのデバイスでのパフォーマンスに関してもほとんど参考になりません。また、JIT ありとなしのデバイスでは大幅な違いがあります。JIT ありのデバイスで "ベスト" なコードが、なしのデバイスで必ずしも "ベスト" とは限りません。
指定のデバイスでアプリがどのように動作するのか知りたい場合は、デバイスでのテストが必要になります。
余計なオブジェクトの生成は避ける
オブジェクトの生成は決して自由ではありません。テンポラリオブジェクト用のアロケーションプールをスレッドごとに持つ世代管理により、アロケーションのコストは低く抑えられてはいますが、メモリをアロケーションするということは、メモリをアロケーションしないよりは常にコストが高くなります。
ユーザインターフェイスのループ内でオブジェクトをアロケーションした場合、ユーザエクスペリエンスでちょっとした "しゃっくり" を作成して、定期的なガベージコレクトを強制することになります。Gingerbread で導入されたコンカレントコレクタにより救われますが、不要な処理は常に避けるべきです。
このように、必要のないオブジェクトインスタンスの生成は避けるべきです。以下は役に立ついくつかの例です。
- 文字列を返すメソッドがあり、その結果は常に StringBuffer に追加する処理であることが分かっている場合は、シグニチャと実装を変更して、短命なテンポラリオブジェクトを生成するのではなく、その関数で直接追加するようにします。
- 入力データのセットから文字列を抽出する際、コピーを作成するのではなく、オリジナルデータのサブストリングを返すようにします。新しい String オブジェクトを生成することにはなりますが、データを持つ char[] で共有されます ( オリジナルの入力のほんの一部分のみを使用する場合はトレードオフとなり、このルートに通るとメモリにずっと保持されてしまいます ) 。
ちょっと極端なアイディアなのですが、多次元配列を並列な 1 次元配列に分解します。
- int の配列が Integer の配列よりははるかに効率がいいのですが、int の配列を 2 つ並べることも ( int, int ) 配列のオブジェクトに比べてずっと効率がいいという事実が一般化しています。これはどのプリミティブ型の組み合わせでも同じことです。
- ( Foo , Bar ) オブジェクトの要素を保持するコンテナを実装する必要がある場合は、一般に Foo[] と Bar[] のふたつの配列の方が ( Foo , Bar ) オブジェクトをカスタマイズしたひとつの配列よりはるかにいいということを思い出すように心がけてください ( もちろん、他のコードからアクセスされる API を設計しているときはその限りではありません。そのケースでは、スピードを少しでも稼げる正しい API の設計に取り替えるのが、普通はいいでしょう。 ですが、内部のコードではできる限り効率的になるように努めてください ) 。
一般的に言うと、できる限り短命のテンポラリオブジェクトの生成は避けてください。生成されるオブジェクトが少なければそれだけガベージコレクションの頻度が減り、それがユーザの操作性を直接左右します。
パフォーマンスの迷信
前のバージョンのドキュメントでは、さまざまな誤解により苦情を受けました。ここではその対応を挙げておきます。
JIT なしのデバイスでは、実際の型の変数でメソッドを呼び出す方が、インターフェイスよりは少しは効率的でした ( つまり、例えば
HashMap map のメソッド呼び出しの方が Map map よりも負荷は少なく、両方のマップが HashMap であってもそうでした ) 。2 倍とまで遅くなることはなく、実際の違いは 6 % ちょっと遅くなるくらいでした。しかも JIT があると、事実上ふたつの違いが分からないくらいです。
JIT なしのデバイスでは、キャッシュしたフィールドへのアクセスの方が繰り返しのフィールドアクセスに比べ約 20 % 速くなります。JIT ありでは、フィールドアクセスの負荷はローカルアクセスと同じなので、コードが読みづらくなるのくらいなら、最適化する価値はなくなってしまいます ( これは final、static、static final の変数でもあてはまります ) 。
仮想よりも Static を優先する
オブジェクトのフィールドにアクセスする必要がない場合は、そのメソッドを static にしてください。呼び出しが 15% から 20% 速くなります。またメソッドのシグニチャーからそのメソッドがオブジェクトの状態を変化させないということを知らせることができるので方策です。
内部の Getter や Setter は避ける
C++ のようなネイティブ言語では、ゲッター ( 例、
i = getCount() ) を使用して、直接フィールドにアクセス ( i
= mCount ) しないのが一般的な習慣となっています。これは、通常はコンパイラがアクセスをインライン化し、必要に応じてアクセス制限したりフィールドをデバッグしたりする必要が生じた場合に、いつでもコードに追加できることから、 C++ では優れた習慣です。
Android では、これがまずい考えです。実際のメソッド呼び出しは、フィールドの呼び出しに比べはるかに負担がかかります。オブジェクト指向言語のプログラミング手法に従い、ゲッターとセッターを public のインターフェイスで持たせるのが理にかなっていますが、クラスの中では常にフィールドに直接アクセスすべきです。
JIT がない場合、直接フィールドにアクセスする方が、平凡なゲッターの呼び出しに比べて 3 倍速くなります。JIT がある場合 ( JIT があると直接フィールドにアクセスするのはローカルにアクセスするのと同じくらいわずかな負荷で済みます ) 、直接フィールドにアクセスする方が、平凡なゲッターの呼び出しに比べて 7 倍速くなります。Froyo がそれに該当しますが、将来的には JIT が getter メソッドをインライン化したときに改善されることになります。
定数には 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 ファイル内の static フィールドイニシャライザに収まることから、このクラスには <clinit> メソッドが不要となります。intVal を参照するコードは 42 の値を直接使用することになり、strVal へのアクセスはフィールドのルックアップではなく比較的低コストの "文字列定数" への命令が使用されることになります ( この最適化は、プリミティブ型と String 定数にのみ適用され、任意の参照型には適用されないので注意してください。それでもできるときは定数を static final で宣言することはよい習慣です ) 。
拡張 For ループ構文を使用する
拡張 For ループ ( また "for-each" ループとしても知られています ) は、Iterable インターフェイスを実装したコレクションと配列に使用することができます。コレクションでは hasNext() と next() のインターフェイスの呼び出しがイテレータに割り当てられています。ArrayList ではカウントされた手書きのループの方が 3 倍速いのですが ( JIT ありでもなしでも ) 、他のコレクションでは拡張 For ループにより、明示的なイテレータの使い方とまったく同じ速さになります。
配列を使ったイテレーションには以下のようにいくつか異なるやり方があります。
static class Foo {
int mSplat;
}
Foo[] mArray = ...
public void zero() {
int sum = 0;
for (int i = 0; i < mArray.length; ++i) {
sum += mArray[i].mSplat;
}
}
public void one() {
int sum = 0;
Foo[] localArray = mArray;
int len = localArray.length;
for (int i = 0; i < len; ++i) {
sum += localArray[i].mSplat;
}
}
public void two() {
int sum = 0;
for (Foo a : mArray) {
sum += a.mSplat;
}
}
zero() はループのすべてのイテレーションに対し配列の長さを取り出しているコストを無視しているので、JIT により最適化されないことから、もっとも遅くなります。
one() はそれよりは速くなります。すべてをローカルの変数に移し、毎回見に行かないようにしています。配列の長さの分のみがパフォーマンに効果をもたらします。
two() は JIT なしのデバイスでもっとも高速で、JIT ありのデバイスでの one() と見分けがつかないほどです。これは Java プログラミング言語のバージョン 1.5 で導入されたループの構文です。
要約すると、パフォーマンス重視の ArrayList のイテレーションのためには、カウントされた手書きのループを考えるのではなく、デフォルトで拡張 For ループを使用してください。
( Effective Java のアイテム 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);
}
}
ここで注目すべき重要な点は、外側のクラスで private のメソッドと private のインスタンスフィールドに直接アクセスしている private のインナークラス ( Foo$Inner ) を定義しているところです。これは規約に合っていて、このコードにより "Value is
27" が表示されることが期待されます。
Foo の private メンバーに Foo$Inner からアクセスしていますが、Java 言語ではインナークラスから外のクラスの private メンバーにアクセスすることは許されているものの、Foo と Foo$Inner が異なるクラスであるという理由から規約に合っていないと VM が見なすことが問題となります。このギャップを埋めるためにコンパイラは人工的に以下の 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 メソッドの呼び出しが必要なときは、常にこれらの static メソッドを呼び出します。結局このコードだと実質的にはアクセサメソッドを介してメンバーフィールドアクセスしていることになります。最初の方で、直接アクセルよりもアクセサの方が遅くなるしくみを述べましたが、このちょっとした言語の語法により "目に見えない" パフォーマンスへの打撃を受てしまう結果に至る一例がこれです。
パフォーマンスがもっとも求められる場所でこのコードを使用する場合、プライベートアクセスではなくパッケージアクセスにすることで、インナークラスでアクセスされるフィールドやメソッドを宣言することによるオーバーヘッドを避けることができます。残念ながらこのフィールドには、同じバッケージの他のクラスにより直接アクセスすることができるので、これを公開の API として使用すべきではありません。
よく考えて浮動小数点を使用する
大ざっぱに言うと、Android デバイスでは浮動小数点は int 値よりも 2 倍遅くなります。これは、FPU なし、JIT なしの G1 と FPU と JIT がある Nexus One でも同じことです ( もちろん 2 つのデバイスの演算処理に対し、絶対的な速度は約 10 倍の差があります ) 。
速度という観点で、最新のハードウェアで float と double の違いはありません。領域的には double が 2 倍の大きさです。デスクトップのマシンでは、この領域の違いは問題にはなりませんが、float よりは double にすべきでしょう。
また int 値であっても、チップによってはハードウェア乗算があり、ハードウェア除算がないものがあります。そのようなケースでは、int 値の除算と剰余はソフトウェアで処理されます。ハッシュテーブルを設計したり、数多くの計算をする場合に配慮が必要となります。
ライブラリを理解して使用する
自分でコードをどうにかするよりも、ライブラリの方がよりよいというあたりまえの理由だけでなく、システムはそのやり方でアセンブラのコードを使って自由にライブラリのメソッドを呼び換えることができ、JIT により Java と等価の生成された最適なコードよりもむしろ良いコードとなることがあるということに留意しておいてください。典型的な例としては String.indexOf とその仲間があり、Dalvik はこれらをインライン化された固有コードに置換します。同様に System.arraycopy メソッドは JIT ありの Nexus One で手動ループの約 9 倍速くなります。
( Effective Java のアイテム 47 も参照してください )
よく考えてネイティブメソッドを使用する
ネイティブコードが必ずしも Java より効率的であるとは限りません。一例を挙げると、Java とネイティブの間の遷移に関連するコストがあり、JIT はそれらの境界の間で最適化することができません。ネイティブリソース ( ネイティブヒープ上のメモリ、ファイル記述子、などそういったもの ) をアロケートすると、それらのリソースの集めて整理することが極めて難しくなる可能性があります。また、それを実行することを望むアーキテクチャそれぞれに対し、コードをコンパイルする必要もあります ( JIT を使ってその実行を委ねるのではなく ) 。同じアーキテクチャであると思っていても、それに対して複数のバージョンをコンパイルする必要がある可能性もあります。G1 で ARM プロセッサ用にコンパイルされたネイティブコードは Nexus One での ARM で最大限に利点を生かすことができず、Nexus One で ARM 用にコンパイルされたコードは G1 での ARM では動作しなくなります。
ネイティブコードは、Android に載せたい既存のコードベースがある際に、特に利用価値があるのであって、Java アプリの部品を "スピードアップ" するためのものではありません。
どうしてもネイティブコードを使用する必要がある場合は、JNI のヒント集 を読んでおいた方がいいでしょう。
( Effective Java のアイテム 54 も参照してください )
あとがき
最後にひとつ。常に測定してください。最適化を開始する前に、問題を確認してください。必ず既存のパフォーマンスを正確に測定してください。そうしないと試そうとしている代替手段の恩恵を計ることができなくなってしまいます。
このドキュメントで求めていることのすべてはベンチマークにより裏付けされています。これらのベンチマークのソースは code.google.com "dalvik" プロジェクト で入手可能です。
このベンチマークは Caliper マイクロベンチマークフレームワーク for Java でビルドされています。マイクロベンチマークは理解するのが難しいので、Caliper がその難しい部分の作業を支援し、測定していると思っている部分が測定されていないケースがある場合 ( 例えば VM コードのすべてを最適化しているからです ) にはその場所を検知してくれます。Caliper を使って独自のマイクロベンチマークを実行することを強くお勧めします。
分析には Traceview も便利かもしれないとお気付きかもしれないですが、これにより JIT が無効となってしまうことに気付いておくことが重要で、JIT があればコードの時間を取り戻せるのではないかと勘違してしまう原因となる可能性があります。Traceview データにより提案された変更を行った後は、その結果のコードが Traceview なしで実行したときには、実際に速くなっているということを確認しておくことが特に重要となります。