Androidデバイスは組み込み系デバイスであるということに疑いの余地はありません。近年の携帯端末は単純な電話というよりも小さな携帯用コンピュータと言えるかもしれませんが、それらのうちで最速の端末でも、処理能力はデスクトップ機の足元にも及びません。
そのため、Androidアプリケーションを書く際にはパフォーマンスを考慮することが非常に重要となります。また、これらのシステムにおいては高速動作が求められるばかりではなくバッテリを長持ちさせることも重要です。つまり、リソースが限られている中では出来る限り効率的なコードを書く必要があるということになります。
本稿ではAndroidのコードを効率的に動作させるためのテクニックを数多く紹介します。これらに従うことで、自信を持って効率的なコードを書くことが出来るでしょう。
目次
リソースの限られたシステムにおいて、まず忘れてはいけないポイントが2つあります。
以下に挙げる全てのヒントは、この2点に従った考え方です。
本稿に書かれているヒントを活用することで、『早すぎる最適化[※1] 』の頻発を危惧される方が居るかもしれません。こまごました最適化により効率的なデータ構造やアルゴリズムの開発が困難になることがあるのは事実ですが、組み込みデバイスにおいては他の選択肢が存在しない場合も多々あります。例えば、デスクトップマシン上のVMと同様にAndroid実機を利用出来ると想定していると、実機上ではメモリを使い尽くしてしまうことになりかねません。つまり、実機で動作させることを考えると、他のアプリケーションとの共存環境上でいかに動作させるかが重要と成ります。
本ガイドラインの存在する意義はここにあります。Android自体の成否はアプリケーションが提供するユーザ体験にかかっており、ユーザ体験はアプリケーションが高い応答性を持ちキビキビ動くか、それとも実行が遅くイライラするかという点にかかっています。また全てのアプリケーションが同一のデバイス上で動作するため、我々はある意味運命共同体と言えます。このドキュメントは運転免許証を取得するために学ぶ道路交通法と同様のものと考えて下さい。これに皆が従っているうちは全てがうまく行くものの、従わなければ悲惨な事故を引き起こしてしまうことになります。
細かな点の説明へ入る前に、1点注意して頂きたいことがあります。以下で説明するほとんどのポイントは、VMがJITコンパイラを持つか否かに関わらず成立するものです。同じことを出来る2つのメソッドがあり、翻訳されたfoo()の実行がbar()より早かったとすればコンパイルをかけたfoo()も、おそらく同じくコンパイルをかけたbar()より高速に動作するでしょう。コンパイラに頼り切ってコードを十分高速動作するようにしてもらおうというのは、甘い考えです[※2] 。
オブジェクト生成には、もちろんコストがかかります。時系列GC[※3] を用いて一時オブジェクト用にスレッドごとのアロケーションプールを利用するとマシになりますが、いずれにしてもメモリを割り当てないよりも割り当てたほうがコスト高に決まっています。
もしユーザインターフェイスループ内でオブジェクトのアロケートを行う場合、定期ガベージコレクションによってユーザに動作が一瞬もたつくような感覚を与えてしまうことになります。
よって、不要なオブジェクトインスタンスを作成するべきではありません。この部分でヒントになりそうな例をいくつか挙げておきます。
この流れをより徹底する場合には、多次元配列を並列した1次元配列群に展開してしまうという方法もあります。
一般に、寿命の短いテンポラリオブジェクトの作成は避けるべきです。オブジェクトを生成する数が少なければ少ないほど、ユーザ体験に直接的な悪影響を及ぼすガベージコレクションの頻度を下げることが出来ます。
文字列操作を行う際には、String.indexOf()やString.lastIndexOf()、といった組み込みメソッドを利用して下さい。これらのメソッド内部はC/C++コードで実装されており、Javaのループを用いて実装する場合の10倍~100倍もの速度で動作します。
この方法のデメリットは、ネイティブメソッドの呼び出しには通常の独自メソッド呼び出しよりも時間がかかるということです。そのため、もし不要であればどうでも良い処理にネイティブメソッドを使わないほうが良いでしょう。
HashMapはよく使われると思います。これは以下のようにHashMapとして、あるいはMapとして宣言することが可能です。
Map myMap1 = new HashMap(); HashMap myMap2 = new HashMap();
どちらがよいのでしょう?
通常の場合は実体クラスがどのようなものであれMapインターフェイスを実装するものに置き換え可能であるためMapを使うべきである、と言われます。通常の環境においては素晴らしい習慣なのですが、組み込み向けシステムにおいては事情が異なります。インターフェイス参照越しでの呼び出しには仮想メソッドを用いた実体参照呼出しの2倍もの時間がかかることが考えらるためです。
HashMapが、あなたの想定する用途にマッチするために利用しているならば、Mapとして呼び出すメリットはあまりありません。リファクタリング機能のあるIDEを用いると、実際のコードがどこにあるかが分からなくてもMap利用部分を書き換えることが出来ます[※4] (繰り返しになりますが、表に公開するためのAPIだけは例外です。良いAPIを提供するためには多少のパフォーマンスを犠牲にしてもやむを得ません)。
オブジェクトのフィールドにアクセスする必要が無ければ、メソッドをstaticにしてください。この方法のほうが、仮想メソッドテーブルの検索が不要である分高速に呼び出されます。また、こうしておくことは当該メソッドがオブジェクト本体に触ることは無い、ということを明示することにも役立ちます。
C++などのネイティブ言語ではフィールドに直接アクセス(例:I = mCount)するよりもゲッターを用いる(例:I = getCount())ことが一般的です。これはC++プログラマにとっては非常に良い習慣で、コード自体は通常コンパイラがインライン化してくれますし、フィールドアクセスの制限やデバッグをいつでも出来るので便利です。
しかしAndroidにおいては、避けるべき習慣です。仮想メソッド呼び出しはインスタンス内のフィールド検索よりもリソースを必要とするためです。オブジェクト指向プログラミングの流儀に則って外部公開用のゲッター/セッターを用意することまでは問題ありませんが、クラス内では常にフィールドへの直接アクセスを行うべきです。
ローカル変数へのアクセスのほうが、メンバフィールドへのアクセスよりも高速です。以下のように書くよりも
for (int i = 0; i < this.mCount; i++)
dumpItem(this.mItems[i]);
以下のように書くべきです。
int count = this.mCount;
Item[] items = this.mItems;
for (int i = 0; i < count; i++)
dumpItems(items[i]);
(ここではthisを用いてオブジェクトのフィールドを明示しています)
同様のガイドラインとして、for構文の第2句においてメソッド呼び出しを行わないこと、というものがあります。例えば、以下のコードは単純なintのキャッシュ利用で済む部分にも関わらずループのたびにgetCount()メソッドを呼び出しているので、大変無駄です。
for (int i = 0; i < this.getCount(); i++)
dumpItems(this.getItem(i));
また、インスタンスのフィールドに2回以上アクセスを行う場合はローカル変数を作成するというのも良いでしょう。例えば以下のようになります。
protected void drawHorizontalScrollBar(Canvas canvas, int width, int height) {
if (isHorizontalScrollBarEnabled()) {
int size = mScrollBar.getSize(false);
if (size <= 0) {
size = mScrollBarSize;
}
mScrollBar.setBounds(0, height - size, width, height);
mScrollBar.setParams(
computeHorizontalScrollRange(),
computeHorizontalScrollOffset(),
computeHorizontalScrollExtent(), false);
mScrollBar.draw(canvas);
}
}
上記のものは、mScrollBarメンバについて、4回の検索がかけられます。この部分は、mScrollBarをローカルのスタック変数にキャッシュすることで、4回のスタック変数参照に置き換えることが出来、より効率的となります。
同様に、メソッドの引数はローカル変数と同様のパフォーマンス特性を持ちます。
クラス上で以下のような宣言を行う場合を考えてみましょう。
static int intVal = 42; static String strVal = "Hello, world!";
このとき、コンパイラはクラスの初回利用時に呼び出される<clinit>というクラス初期化メソッドを生成します。このメソッド内ではintValに42を代入し、クラスファイルの文字列定数テーブルから参照を取り出してstrValに割り当てます。これらの値が参照されるときには、フィールド検索が実行されることになります。
この状況は、finalキーワードにより改善することが出来ます。
static final int intVal = 42; static final String strVal = "Hello, world!";
こうすると、定数値がVMによる直接アクセスの行われる、クラスファイルのスタティックフィールド初期化子により管理されるようになるため、クラス初期化のための<clinit>メソッド自体が不要となります。この場合intValへのアクセスは直接42という値を利用するようになり、strValへのアクセスはfinal無しのstatic宣言時に用いられるフィールド検索よりも低コストな文字列定数参照により処理出来るようになります。
メソッドやクラスをfinal宣言してもパフォーマンス上の直接的なメリットは得にくいのですが、ある種の最適化は施されるようになります。例えば、ゲッターメソッドがサブクラスにおいてオーバーライドされないことが分かっていれば、そのメソッドはインライン化することが出来ます。
同様にローカル変数をfinal宣言することも可能ですが、この場合パフォーマンス上のメリットは全く無く、コードの意味を明確化することに役立つのみとなります(あるいは、匿名内部クラスでの利用において必要となる場合もあります)。
foreach構文はIterableインターフェイスを実装するコレクションに対して利用することが出来ます。これらのオブジェクトに対してforeachはイテレータを割当て、hasNext()とnext()メソッドを呼び出します。ArrayListにおいてはこの仕組みを介さない方が高速となりますが、他のコレクションについては通常のイテレータ利用と全く同じと考えられます。
注意点を明確にするための、下記のforeachを実現するサンプルをご覧下さい。
public class Foo {
int mSplat;
static Foo mArray[] = new Foo[27];
public static void zero() {
int sum = 0;
for (int i = 0; i < mArray.length; i++) {
sum += mArray[i].mSplat;
}
}
public static void one() {
int sum = 0;
Foo[] localArray = mArray;
int len = localArray.length;
for (int i = 0; i < len; i++) {
sum += localArray[i].mSplat;
}
}
public static void two() {
int sum = 0;
for (Foo a: mArray) {
sum += a.mSplat;
}
}
}
zero() ではスタティックフィールドを2回取得すると共に、ループ中で毎回配列長を取得しています。
one() ではフィールド検索を避けつつ全てのデータをローカル変数に取り込んでいます。
two() ではJavaプログラミング言語の1.5で導入されたforeach構文を利用しています。コンパイラによって生成されたコードは効率よく配列内の全要素を辿っていけるように配列参照のコピーや配列長のローカル変数割当を管理してくれます。一方この場合”a”が先頭に付くデータ保管用のローカル変数が追加され、若干のパフォーマンス低下とone()メソッドよりも4バイト多く[※5] メモリを消費することになります。
ごちゃごちゃしてしまったのでまとめると、foreach構文は配列に対して有力であるものの、Iterableなオブジェクトに対して利用する場合には追加のオブジェクト生成が必要となる点に注意が必要、ということです。
Enumは非常に便利ですが、容量と速度、という面では最悪です。例えば以下のようなものですが
public class Foo {
public enum Shrubbery { GROUND, CRAWLING, HANGING }
}
これは900バイトもの.classファイル(Foo$Shubbery.class)にコンパイルされ、初回呼び出し時にクラス初期化子が各列挙値に対応するオブジェクトについて<init>メソッドを呼び出します。各オブジェクトが独自のスタティック値を持ち、それらは総体として配列内に保管されます($VALUESというスタティックフィールドとなります)。たった3つの整数値で代替出来るものに対してこの処理は大げさすぎます。
Shrubbery shrub = Shrubbery.GROUND;
上記コードの実行にはスタティックフィールド検索が必要となります。仮に"GROUND"がstatic final intとなっていれば、コンパイラはこれを定数として扱い、インライン展開出来ます。
一方でもちろん、外部へ公開するAPIの作りこみ時やコンパイル時の値チェックを行いたいケースでは有用です。結局他の項目と同様、公開API用にはEnumを利用すべきであるが、パフォーマンス上の問題から極力利用を避けるべき、という具合です。
場合によってはEnumのint値をordinal()メソッド経由で取得する機能が便利に使えるかもしれません。例えば以下のようなコードは
for (int n = 0; n < list.size(); n++) {
if (list.items[n].e == MyEnum.VAL_X)
// do stuff 1
else if (list.items[n].e == MyEnum.VAL_Y)
// do stuff 2
}
このように書くことが出来ます。
int valX = MyEnum.VAL_X.ordinal();
int valY = MyEnum.VAL_Y.ordinal();
int count = list.size();
MyItem items = list.items();
for (int n = 0; n < count; n++){
int valItem = items[n].e.ordinal();
if (valItem == valX)
// do stuff 1
else if (valItem == valY)
// do stuff 2
}
この記法のほうが高速動作するはずですが、常に速いと保証されているわけではありません。
以下のクラス定義を考えてみてください
public class Foo {
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 class Inner {
void stuff() {
Foo.this.doStuff(Foo.this.mValue);
}
}
}
特に注目すべきは、内部クラス(Foo$Inner)を定義し、その中でprivateなメソッドに直接アクセスしたり、外側のクラスインスタンスフィールドにアクセスしている点です。この記述は正しく、きちんと『Value is 27』と表示されます。
問題は、Foo$Innerが技術的には外側のクラスと全く別物であるため、Fooのプライベートメンバへの直接アクセスが禁止されるということです。この不便を防ぐために、コンパイラは以下のような対応メソッド群を生成することになります。
/*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メソッドの呼び出しも、全てこの枠組みの中で行われます。これは、メンバフィールドに対して直接アクセスではなくアクセッサメソッドを用いてアクセスしている場合に、特に凶悪なものとなります。本稿内で既に、アクセッサの内部利用がフィールドへの直接アクセスより低速であるかについてご説明しましたが、ここで挙げた例は目に見えないパフォーマンス悪化を引き起こす定型パターンです。
この問題の本質的解決方法は、内部クラスからのアクセスを受けるフィールドやメソッドをprivateスコープではなくpackageスコープにて宣言することです。これにより、privateスコープ時よりも高速動作し、生成されるメソッドによるオーバーヘッドを回避することが出来ます。メリットの裏返しですが、この方法を用いることは全てのフィールドをprivateにすべし、というオブジェクト指向の方向性に反するものでもあります。
例によって、公開APIを設計する際にはこの最適化を行うか否かについて慎重な検討が必要となります。
Pentium CPUのリリース前には、ゲーム開発者にとって整数演算で出来る限りのことを行うことが一般的でした。Pentiumの登場により浮動小数点コプロセッサが組み込まれ、整数と浮動小数点数の両方を用いた制御のほうが整数演算のみよりも高速となりました。しかし例によって組み込み系の事情はデスクトップシステムの事情と異なる点もあります。
残念ながら、組み込み用プロセッサには浮動小数点数演算用のハードウェアが含まれていない場合が多々あります。そのため、floatとdoubleの処理は全てソフトウェア的に実行されます。簡単な浮動小数点数機能のうちいくつかは、処理完了までにミリ秒単位の時間が必要なものもあります。
また、整数値においてさえもチップによって、ハードウェア掛け算機能はあれどもハードウェア割り算機能は無い環境もあります。この場合、整数割り算とmod計算においてはソフトウェア処理を行うことになります。このことは、ハッシュテーブル設計や数学的要素を利用する際などのために知っておく必要があります。
我々の考え方を明確にするために、いくつかの簡単なアクションについて大体の所要時間を表に示します。これらの値は絶対的なものではないことに注意が必要です。これらはCPU上の時間と実時間の感覚をミックスしたものとなっており、[※6] システムの改善により変化することが考えられます。しかしながら、これらの行動が相対的にどの程度処理を要するかについて一つの指標を提供することは重要でしょう。例えば、メンバ変数の追加はローカル変数よりも4倍もの時間が必要、という具合です。
| アクション | 時間 |
|---|---|
| ローカル変数を追加する | 1 |
| メンバ変数を追加する | 4 |
| String.length()を呼び出す | 5 |
| 空のstaticネイティブメソッドを呼び出す | 5 |
| 空のstaticメソッドを呼び出す | 12 |
| 空の仮想メソッドを呼び出す | 12.5 |
| 空のインターフェイスメソッドを呼び出す | 15 |
| HashMap上でIterator:next()を呼び出す | 165 |
| HashMap上でput()を呼び出す | 600 |
| XMLから1つのビューを生成する | 22,000 |
| 1つのTextViewを含む1つのLinearLayoutを生成する | 25,000 |
| 6つのビューオブジェクトを含む1つのLinearLayoutを生成する | 100,000 |
| 6つのTextViewを含む1つのLinearLayoutを生成する | 135,000 |
| 空のアクティビティを起動する | 3,000,000 |
組み込みシステムにおいて良いコード、効率的なコードを書くための最良の道は、自分の書いているコードで何が起こるかを正確に知ることです。どうしてもListに対してforeach構文を適用するためのイテレータを生成したければ、しっかりと考えた上で行うべきです。[※7]
実際のアプリケーション構築にかかるまえにこのページを読まれたということは、今後のアプリケーション構築において強力な武器を得たということに他なりません。とにかく、常に自分の書いたコードがどのような結果をもたらすのか、そしてどのようにすれば実行を高速化出来るのか、と考えるようにして下さい。