効率性を高める設計を行う

Androidアプリケーションは高速に動作しなければなりません。より正確には効率的に動作する必要がある、と言えます。 つまり、モバイルデバイスの限られた計算資源、データストレージ、小さな画面、お世辞にも多いとは言えないバッテリという環境においては可能な限り効率的な実行が求められます。

アプリケーションを開発する際には、たとえアプリケーションがデュルコア環境のエミュレータ上で高速に動作したとしてもモバイルデバイス上ではそのようにうまくは動作しないということに 留意しておく必要があります。最も強力なモバイルデバイスでも通常のデスクトップ環境にはかなわないでしょう。 そのため、極力多くのモバイルデバイス上で快適に動作することを目指す上で効率的なコードを書くことが重要となります。

一般に、高速な、あるいは効率的なコードを書くということはメモリの割当量を極小化し、最適化されたコードを書き、 パフォーマンスを犠牲にしうる特定の言語やプログラミング上のイディオムを避けることを指します。オブジェクト指向において これらの多くはメソッドレベルでの対策(コード行数の削減やループ処理など)となります。

本ドキュメントでは以下のトピックを扱います。

イントロダクション

リソースの限られたシステムにおいて、まず忘れてはいけないポイントが2つあります。

以下に挙げる全てのヒントは、この2点に従った考え方です。

本稿に書かれているヒントを活用することで、『早すぎる最適化』の頻発を危惧される方が居るかもしれません。 こまごました最適化により効率的なデータ構造やアルゴリズムの開発が困難になることがあるのは事実ですが、 携帯端末のような組み込みデバイスにおいては他の選択肢が存在しない場合も多々あります。例えば、デスクトップマシン上のVMと同様に Android実機を利用出来ると想定していると、実機上ではメモリを使い尽くしてしまうことになりかねません。 これによりアプリケーションが動作しなくなるだけでなくシステム上で動作している他のプログラムにも悪影響を与えることとなります。

本ガイドラインの存在する意義はここにあります。Android自体の成否はアプリケーションが提供するユーザ体験にかかっており、 ユーザ体験はアプリケーションが高い応答性を持ちキビキビ動くか、それとも実行が遅くイライラするかという点にかかっています。 また全てのアプリケーションが同一のデバイス上で動作するため、我々はある意味運命共同体と言えます。このドキュメントは 運転免許証を取得するために学ぶ道路交通法と同様のものと考えて下さい。これに皆が従っているうちは全てがうまく行くものの、 従わなければ悲惨な事故を引き起こしてしまうことになります。

細かな点の説明へ入る前に、1点注意して頂きたいことがあります。以下で説明するほとんどのポイントは、 VMがJITコンパイラを持つか否かに関わらず成立するものです。同じことを出来る2つのメソッドがあり、 翻訳されたfoo()の実行がbar()より早かったとすればコンパイルをかけたfoo()も、おそらく同じくコンパイルをかけたbar()よりも 高速に動作するでしょう。コンパイラに頼り切ってコードを十分高速動作するようにしてもらおうというのは、甘い考えです。

オブジェクトの生成を避ける

オブジェクト生成には、もちろんコストがかかります。時系列GCを用いて一時オブジェクト用にスレッドごとの アロケーションプールを利用するとマシになりますが、いずれにしてもメモリを割り当てないよりも割り当てたほうがコスト高であるのは明らかです。

もしユーザインターフェイスループ内でオブジェクトのアロケートを行う場合、定期ガベージコレクションによって ユーザに動作が一瞬もたつくような感覚を与えてしまうことになります。

ともかく、不要なオブジェクトインスタンスを作成するべきではありません。この部分でヒントになりそうな例をいくつか挙げておきます。

この方針をより徹底する場合には、多次元配列を並列した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利用部分を書き換えることが出来ます(繰り返しになりますが、表に公開される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回のスタック変数参照に置き換えることが出来、より効率的となります。

同様に、メソッドの引数はローカル変数と同様のパフォーマンス特性を持ちます。

定数をfinal定義する

クラス上で以下のような宣言を行う場合を考えてみましょう。

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宣言してもパフォーマンス上の直接的なメリットは得にくいのですが、ある種の最適化は施されるようになります。 例えば、ゲッターメソッドがサブクラスにおいてオーバーライドされないことが分かっていれば、そのメソッドはインライン化することが出来ます。/p>

同様にローカル変数をfinal宣言することも可能ですが、この場合パフォーマンス上のメリットはさほど無く、 コードの意味を明確化することに役立つのみとなります(あるいは、匿名内部クラスでの利用において必要となる場合もあります)。

拡張for構文は注意して利用する

拡張forループ("for-each"ループと呼ばれることもある)はIterableインターフェイスを実装するコレクションに対して利用することが出来ます。 これらのオブジェクトに対して イテレータが割り当てられ、hasNext()とnext()メソッドを呼び出します。ArrayListにおいてはこの仕組みを介さない方が高速となりますが、 他のコレクションについては通常のイテレータ利用と全く同じとなります。

以下に、拡張for構文の正しい使い方を理解するための例を示します

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で導入された拡張forループ構文を利用しています。 コンパイラによって生成されたコードは効率よく配列内の全要素を辿っていけるように配列参照のコピーや配列長のローカル変数割当を管理してくれます。 一方この場合"a"が先頭に付くデータ保管用のローカル変数が追加され、若干のパフォーマンス低下とone()メソッドよりも4バイト多くメモリを消費することになります。

ごちゃごちゃしてしまったのでまとめると、foreach構文は配列に対して有力であるものの、 Iterableなオブジェクトに対して利用する場合には追加のオブジェクト生成が必要となる点に注意が必要、ということです。

Enumを避ける

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を設計する際にはこの最適化に関して注意深く検討する必要があります。

floatを避ける

Pentium CPUのリリース前には、ゲーム開発者にとって整数演算で出来る限りのことを行うことが一般的でした。Pentiumの登場により浮動小数点コプロセッサが組み込まれ、 整数と浮動小数点数の両方を用いた制御のほうが整数演算のみよりも高速となりました。結果的に現在、デスクトップ環境では浮動小数点演算を 自由軽に使うことが出来ます。

しかし残念ながら組み込み用プロセッサではハードウェアでの浮動小数点演算をサポートしていないものが多くあります。 そのため、"float"や"double"での処理はソフトウェアによって行われます。基本的な浮動小数点演算の中には 数ミリ秒もの時間がかかってしまうものがあります。

また、整数値においてさえもチップによって、ハードウェアで乗算をサポートしていても除算をサポートしていない場合があります。 この場合、整数の除算とmod演算はソフトウェアで行われることとなります。このことは、ハッシュテーブル設計や数学的演算を 行う場合などのために知っておく必要があります。

パフォーマンス値の例

我々の考え方をイメージしやすいように、いくつかの簡単なアクションについて大体の所要時間を表に示します。 これらの値は絶対的なものではないことに注意が必要です。これらはCPU上の時間と実時間の感覚をミックスしたものとなっており、 システムの改善により変化することが考えられます。しかしながら、これらの行動が相対的にどの程度処理を要するかについて一つの指標を提供することは重要でしょう。 例えば、メンバ変数の追加はローカル変数よりも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に対して拡張for構文を適用するためのイテレータを生成する必要があるのなら、しっかりと考えた上で行うべきです。

実際のアプリケーション構築にかかるまえにこのページを読まれたということは、今後のアプリケーション構築において強力な武器を得たということに他なりません。 とにかく、常に自分の書いたコードがどのような結果をもたらすのか、そしてどのようにすれば実行を高速化出来るのか、と考えるようにして下さい。